Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Bennett <lukeeeebennettplus@gmail.com>2018-05-28 14:27:06 +0300
committerLuke Bennett <lukeeeebennettplus@gmail.com>2018-05-28 14:27:06 +0300
commita9583bc9562512bd046a7f2c32fd267f32a5dac2 (patch)
treeaf91d0688d49850746de400afdacb5389c0cfb16
parenta3e472e0a7a6b6ec5654edd20b947ba660ed2dc3 (diff)
parent265b1fafe64ae9fe8a3e92d83c1678b47533ba86 (diff)
Merge remote-tracking branch 'origin/master' into 39549-label-list-page-redesign-with-draggable-labels
-rw-r--r--.babelrc6
-rw-r--r--.flayignore5
-rw-r--r--.gitignore3
-rw-r--r--.gitlab-ci.yml422
-rw-r--r--.gitlab/issue_templates/Security Developer Workflow.md4
-rw-r--r--.gitlab/merge_request_templates/Database Changes.md10
-rw-r--r--.rubocop_todo.yml8
-rw-r--r--.scss-lint.yml3
-rw-r--r--CHANGELOG.md469
-rw-r--r--CONTRIBUTING.md95
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile45
-rw-r--r--Gemfile.lock141
-rw-r--r--Gemfile.rails5.lock190
-rw-r--r--LICENSE26
-rw-r--r--README.md9
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js30
-rw-r--r--app/assets/javascripts/awards_handler.js8
-rw-r--r--app/assets/javascripts/badges/components/badge.vue6
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue10
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js4
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js27
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js6
-rw-r--r--app/assets/javascripts/blob/sketch/index.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js12
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js4
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js5
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js26
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue2
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue4
-rw-r--r--app/assets/javascripts/clusters/components/gcp_signup_offer.js27
-rw-r--r--app/assets/javascripts/commons/bootstrap.js10
-rw-r--r--app/assets/javascripts/compare.js86
-rw-r--r--app/assets/javascripts/compare_autocomplete.js51
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js43
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_service.js30
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue71
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue212
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue322
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue100
-rw-r--r--app/assets/javascripts/deploy_keys/index.js39
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js28
-rw-r--r--app/assets/javascripts/deploy_keys/store/index.js4
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js4
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js2
-rw-r--r--app/assets/javascripts/due_date_select.js9
-rw-r--r--app/assets/javascripts/emoji/index.js6
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js40
-rw-r--r--app/assets/javascripts/environments/components/container.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue20
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue6
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue17
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue5
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue4
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue20
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue3
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js26
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js17
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js9
-rw-r--r--app/assets/javascripts/gl_dropdown.js4
-rw-r--r--app/assets/javascripts/gl_field_error.js10
-rw-r--r--app/assets/javascripts/gpg_badges.js6
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue2
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue106
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue75
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue18
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue36
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue171
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue164
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue121
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue61
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue30
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue59
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue33
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue45
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue80
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue243
-rw-r--r--app/assets/javascripts/ide/components/file_finder/item.vue113
-rw-r--r--app/assets/javascripts/ide/components/ide.vue170
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue84
-rw-r--r--app/assets/javascripts/ide/components/ide_external_links.vue43
-rw-r--r--app/assets/javascripts/ide/components/ide_file_buttons.vue10
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue65
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue41
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue62
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue143
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue134
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue42
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue76
-rw-r--r--app/assets/javascripts/ide/components/mr_file_icon.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue97
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue13
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue170
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue70
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue114
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue50
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue17
-rw-r--r--app/assets/javascripts/ide/constants.js19
-rw-r--r--app/assets/javascripts/ide/ide_router.js25
-rw-r--r--app/assets/javascripts/ide/index.js33
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js43
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js4
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js9
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js25
-rw-r--r--app/assets/javascripts/ide/lib/editor.js45
-rw-r--r--app/assets/javascripts/ide/lib/keymap.json11
-rw-r--r--app/assets/javascripts/ide/services/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js62
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js53
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js82
-rw-r--r--app/assets/javascripts/ide/stores/getters.js62
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js88
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js14
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js49
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/mutations.js53
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/state.js6
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js13
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js57
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js14
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js104
-rw-r--r--app/assets/javascripts/ide/stores/state.js8
-rw-r--r--app/assets/javascripts/ide/stores/utils.js18
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js12
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue2
-rw-r--r--app/assets/javascripts/job.js11
-rw-r--r--app/assets/javascripts/jobs/components/header.vue150
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_detail_row.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue185
-rw-r--r--app/assets/javascripts/jobs/job_details_bundle.js5
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/labels_select.js7
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js98
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js6
-rw-r--r--app/assets/javascripts/main.js69
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/milestone_select.js83
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/path.vue22
-rw-r--r--app/assets/javascripts/monitoring/components/graph/track_line.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue6
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js20
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js1
-rw-r--r--app/assets/javascripts/notes.js10
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue11
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue10
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue35
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js3
-rw-r--r--app/assets/javascripts/notes/stores/getters.js3
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/pages/admin/admin.js2
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue5
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js4
-rw-r--r--app/assets/javascripts/pages/ide/index.js9
-rw-r--r--app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/login/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js54
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js60
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js22
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue77
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js28
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js205
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue20
-rw-r--r--app/assets/javascripts/performance_bar/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue95
-rw-r--r--app/assets/javascripts/pipelines/components/blank_state.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue60
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue135
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_artifacts.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue56
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue107
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue261
-rw-r--r--app/assets/javascripts/pipelines/components/time_ago.vue2
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js4
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js31
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue6
-rw-r--r--app/assets/javascripts/project_fork.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js6
-rw-r--r--app/assets/javascripts/project_visibility.js2
-rw-r--r--app/assets/javascripts/projects/project_new.js4
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue5
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue2
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js14
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js2
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue9
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue16
-rw-r--r--app/assets/javascripts/registry/stores/actions.js3
-rw-r--r--app/assets/javascripts/registry/stores/getters.js3
-rw-r--r--app/assets/javascripts/right_sidebar.js14
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js5
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue21
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue12
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue22
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue33
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js10
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js58
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue61
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js15
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue12
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js3
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js3
-rw-r--r--app/assets/javascripts/user_callout.js9
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue20
-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_conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js67
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue76
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js277
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue291
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/callout.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue75
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue215
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_modal.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue101
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue136
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue123
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue2
-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.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue39
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue4
-rw-r--r--app/assets/javascripts/vue_shared/directives/popover.js2
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js4
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js2
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss149
-rw-r--r--app/assets/stylesheets/emoji_sprites.scss5403
-rw-r--r--app/assets/stylesheets/framework.scss126
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/badges.scss2
-rw-r--r--app/assets/stylesheets/framework/banner.scss2
-rw-r--r--app/assets/stylesheets/framework/blank.scss8
-rw-r--r--app/assets/stylesheets/framework/blocks.scss12
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss6
-rw-r--r--app/assets/stylesheets/framework/buttons.scss13
-rw-r--r--app/assets/stylesheets/framework/calendar.scss2
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss12
-rw-r--r--app/assets/stylesheets/framework/common.scss12
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss30
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss58
-rw-r--r--app/assets/stylesheets/framework/emoji_sprites.scss1813
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss6
-rw-r--r--app/assets/stylesheets/framework/filters.scss20
-rw-r--r--app/assets/stylesheets/framework/flash.scss14
-rw-r--r--app/assets/stylesheets/framework/forms.scss41
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss48
-rw-r--r--app/assets/stylesheets/framework/header.scss37
-rw-r--r--app/assets/stylesheets/framework/images.scss35
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss4
-rw-r--r--app/assets/stylesheets/framework/lists.scss249
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss18
-rw-r--r--app/assets/stylesheets/framework/mixins.scss22
-rw-r--r--app/assets/stylesheets/framework/mobile.scss10
-rw-r--r--app/assets/stylesheets/framework/modal.scss8
-rw-r--r--app/assets/stylesheets/framework/page_header.scss6
-rw-r--r--app/assets/stylesheets/framework/pagination.scss13
-rw-r--r--app/assets/stylesheets/framework/panels.scss17
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss22
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss46
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss15
-rw-r--r--app/assets/stylesheets/framework/snippets.scss5
-rw-r--r--app/assets/stylesheets/framework/tables.scss2
-rw-r--r--app/assets/stylesheets/framework/tabs.scss1
-rw-r--r--app/assets/stylesheets/framework/terms.scss64
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/toggle.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss201
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss199
-rw-r--r--app/assets/stylesheets/framework/typography.scss18
-rw-r--r--app/assets/stylesheets/framework/variables.scss35
-rw-r--r--app/assets/stylesheets/framework/wells.scss7
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss2
-rw-r--r--app/assets/stylesheets/pages/boards.scss50
-rw-r--r--app/assets/stylesheets/pages/builds.scss51
-rw-r--r--app/assets/stylesheets/pages/clusters.scss48
-rw-r--r--app/assets/stylesheets/pages/commits.scss37
-rw-r--r--app/assets/stylesheets/pages/convdev_index.scss28
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss16
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss48
-rw-r--r--app/assets/stylesheets/pages/editor.scss24
-rw-r--r--app/assets/stylesheets/pages/environments.scss45
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss246
-rw-r--r--app/assets/stylesheets/pages/help.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss60
-rw-r--r--app/assets/stylesheets/pages/issues.scss12
-rw-r--r--app/assets/stylesheets/pages/labels.scss5
-rw-r--r--app/assets/stylesheets/pages/login.scss6
-rw-r--r--app/assets/stylesheets/pages/members.scss54
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss10
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss30
-rw-r--r--app/assets/stylesheets/pages/milestone.scss22
-rw-r--r--app/assets/stylesheets/pages/note_form.scss22
-rw-r--r--app/assets/stylesheets/pages/notes.scss43
-rw-r--r--app/assets/stylesheets/pages/notifications.scss2
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss358
-rw-r--r--app/assets/stylesheets/pages/profile.scss12
-rw-r--r--app/assets/stylesheets/pages/projects.scss131
-rw-r--r--app/assets/stylesheets/pages/repo.scss604
-rw-r--r--app/assets/stylesheets/pages/repo.scss.orig786
-rw-r--r--app/assets/stylesheets/pages/runners.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss11
-rw-r--r--app/assets/stylesheets/pages/settings.scss26
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss13
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss12
-rw-r--r--app/assets/stylesheets/pages/tree.scss11
-rw-r--r--app/assets/stylesheets/pages/wiki.scss9
-rw-r--r--app/assets/stylesheets/performance_bar.scss1
-rw-r--r--app/assets/stylesheets/print.scss8
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb6
-rw-r--r--app/controllers/application_controller.rb41
-rw-r--r--app/controllers/boards/issues_controller.rb2
-rw-r--r--app/controllers/concerns/accepts_pending_invitations.rb15
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb3
-rw-r--r--app/controllers/concerns/continue_params.rb4
-rw-r--r--app/controllers/concerns/internal_redirect.rb35
-rw-r--r--app/controllers/concerns/issuable_actions.rb1
-rw-r--r--app/controllers/concerns/issuable_collections.rb6
-rw-r--r--app/controllers/concerns/send_file_upload.rb4
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/boards_controller.rb11
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/groups/runners_controller.rb58
-rw-r--r--app/controllers/groups/settings/badges_controller.rb4
-rw-r--r--app/controllers/import/base_controller.rb11
-rw-r--r--app/controllers/import/bitbucket_controller.rb6
-rw-r--r--app/controllers/import/fogbugz_controller.rb5
-rw-r--r--app/controllers/import/github_controller.rb5
-rw-r--r--app/controllers/import/gitlab_controller.rb5
-rw-r--r--app/controllers/import/google_code_controller.rb5
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb31
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb148
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb14
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/boards_controller.rb9
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb7
-rw-r--r--app/controllers/projects/commit_controller.rb8
-rw-r--r--app/controllers/projects/compare_controller.rb65
-rw-r--r--app/controllers/projects/issues_controller.rb6
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb7
-rw-r--r--app/controllers/projects/mirrors_controller.rb67
-rw-r--r--app/controllers/projects/notes_controller.rb17
-rw-r--r--app/controllers/projects/pipelines_controller.rb48
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb2
-rw-r--r--app/controllers/projects/repositories_controller.rb11
-rw-r--r--app/controllers/projects/runner_projects_controller.rb4
-rw-r--r--app/controllers/projects/runners_controller.rb23
-rw-r--r--app/controllers/projects/settings/badges_controller.rb4
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb12
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb9
-rw-r--r--app/controllers/projects/settings/repository_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb19
-rw-r--r--app/controllers/registrations_controller.rb4
-rw-r--r--app/controllers/sent_notifications_controller.rb20
-rw-r--r--app/controllers/sessions_controller.rb9
-rw-r--r--app/controllers/users/terms_controller.rb70
-rw-r--r--app/finders/group_descendants_finder.rb6
-rw-r--r--app/finders/groups_finder.rb8
-rw-r--r--app/finders/issuable_finder.rb9
-rw-r--r--app/finders/issues_finder.rb2
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/finders/personal_projects_finder.rb2
-rw-r--r--app/finders/pipelines_finder.rb9
-rw-r--r--app/finders/users_finder.rb12
-rw-r--r--app/helpers/active_sessions_helper.rb23
-rw-r--r--app/helpers/application_helper.rb92
-rw-r--r--app/helpers/application_settings_helper.rb5
-rw-r--r--app/helpers/auth_helper.rb8
-rw-r--r--app/helpers/auto_devops_helper.rb9
-rw-r--r--app/helpers/avatars_helper.rb76
-rw-r--r--app/helpers/blob_helper.rb5
-rw-r--r--app/helpers/ci_status_helper.rb8
-rw-r--r--app/helpers/clusters_helper.rb8
-rw-r--r--app/helpers/commits_helper.rb12
-rw-r--r--app/helpers/count_helper.rb9
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb51
-rw-r--r--app/helpers/labels_helper.rb2
-rw-r--r--app/helpers/milestones_helper.rb93
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/helpers/system_note_helper.rb6
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/helpers/users_helper.rb22
-rw-r--r--app/helpers/webpack_helper.rb45
-rw-r--r--app/mailers/emails/merge_requests.rb8
-rw-r--r--app/mailers/emails/notes.rb5
-rw-r--r--app/mailers/notify.rb14
-rw-r--r--app/models/ability.rb8
-rw-r--r--app/models/active_session.rb110
-rw-r--r--app/models/appearance.rb18
-rw-r--r--app/models/application_setting.rb58
-rw-r--r--app/models/application_setting/term.rb13
-rw-r--r--app/models/ci/build.rb43
-rw-r--r--app/models/ci/build_trace_chunk.rb180
-rw-r--r--app/models/ci/job_artifact.rb40
-rw-r--r--app/models/ci/pipeline.rb76
-rw-r--r--app/models/ci/pipeline_variable.rb2
-rw-r--r--app/models/ci/runner.rb83
-rw-r--r--app/models/ci/runner_namespace.rb9
-rw-r--r--app/models/ci/stage.rb21
-rw-r--r--app/models/clusters/applications/prometheus.rb5
-rw-r--r--app/models/clusters/applications/runner.rb10
-rw-r--r--app/models/clusters/cluster.rb2
-rw-r--r--app/models/commit.rb40
-rw-r--r--app/models/commit_status.rb8
-rw-r--r--app/models/concerns/atomic_internal_id.rb5
-rw-r--r--app/models/concerns/avatarable.rb5
-rw-r--r--app/models/concerns/cacheable_attributes.rb54
-rw-r--r--app/models/concerns/diff_file.rb9
-rw-r--r--app/models/concerns/fast_destroy_all.rb91
-rw-r--r--app/models/concerns/group_descendant.rb15
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/milestoneish.rb4
-rw-r--r--app/models/concerns/nonatomic_internal_id.rb22
-rw-r--r--app/models/concerns/participable.rb4
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/reactive_caching.rb17
-rw-r--r--app/models/concerns/redis_cacheable.rb19
-rw-r--r--app/models/concerns/resolvable_discussion.rb2
-rw-r--r--app/models/concerns/resolvable_note.rb2
-rw-r--r--app/models/concerns/routable.rb11
-rw-r--r--app/models/concerns/sha_attribute.rb30
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb30
-rw-r--r--app/models/concerns/time_trackable.rb10
-rw-r--r--app/models/concerns/uniquify.rb22
-rw-r--r--app/models/concerns/with_uploads.rb39
-rw-r--r--app/models/deploy_key.rb2
-rw-r--r--app/models/deploy_token.rb7
-rw-r--r--app/models/deployment.rb4
-rw-r--r--app/models/diff_note.rb54
-rw-r--r--app/models/fork_network.rb2
-rw-r--r--app/models/group.rb54
-rw-r--r--app/models/identity.rb8
-rw-r--r--app/models/internal_id.rb3
-rw-r--r--app/models/issue.rb18
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/lfs_object.rb8
-rw-r--r--app/models/list.rb3
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/group_member.rb6
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb39
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/merge_request_diff_file.rb7
-rw-r--r--app/models/milestone.rb7
-rw-r--r--app/models/namespace.rb14
-rw-r--r--app/models/note.rb8
-rw-r--r--app/models/note_diff_file.rb7
-rw-r--r--app/models/project.rb314
-rw-r--r--app/models/project_ci_cd_setting.rb16
-rw-r--r--app/models/project_import_state.rb55
-rw-r--r--app/models/project_services/drone_ci_service.rb2
-rw-r--r--app/models/project_services/flowdock_service.rb48
-rw-r--r--app/models/project_services/gemnasium_service.rb13
-rw-r--r--app/models/project_statistics.rb20
-rw-r--r--app/models/project_wiki.rb8
-rw-r--r--app/models/protected_branch.rb9
-rw-r--r--app/models/remote_mirror.rb219
-rw-r--r--app/models/repository.rb59
-rw-r--r--app/models/sent_notification.rb4
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/storage/hashed_project.rb4
-rw-r--r--app/models/storage/legacy_project.rb8
-rw-r--r--app/models/system_note_metadata.rb6
-rw-r--r--app/models/term_agreement.rb6
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb120
-rw-r--r--app/models/user_callout.rb3
-rw-r--r--app/models/wiki_page.rb18
-rw-r--r--app/policies/application_setting/term_policy.rb28
-rw-r--r--app/policies/ci/build_policy.rb9
-rw-r--r--app/policies/ci/pipeline_policy.rb8
-rw-r--r--app/policies/ci/runner_policy.rb15
-rw-r--r--app/policies/global_policy.rb19
-rw-r--r--app/policies/group_policy.rb8
-rw-r--r--app/policies/project_policy.rb11
-rw-r--r--app/policies/user_policy.rb6
-rw-r--r--app/presenters/ci/build_presenter.rb4
-rw-r--r--app/presenters/ci/pipeline_presenter.rb10
-rw-r--r--app/presenters/commit_status_presenter.rb24
-rw-r--r--app/presenters/generic_commit_status_presenter.rb2
-rw-r--r--app/presenters/merge_request_presenter.rb11
-rw-r--r--app/presenters/project_presenter.rb19
-rw-r--r--app/serializers/entity_date_helper.rb27
-rw-r--r--app/serializers/job_entity.rb18
-rw-r--r--app/serializers/merge_request_widget_entity.rb7
-rw-r--r--app/serializers/pipeline_entity.rb6
-rw-r--r--app/serializers/project_mirror_entity.rb11
-rw-r--r--app/serializers/stage_entity.rb16
-rw-r--r--app/serializers/stage_serializer.rb7
-rw-r--r--app/services/application_settings/update_service.rb15
-rw-r--r--app/services/boards/issues/create_service.rb6
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--app/services/ci/ensure_stage_service.rb1
-rw-r--r--app/services/ci/register_job_service.rb31
-rw-r--r--app/services/ci/update_build_queue_service.rb16
-rw-r--r--app/services/clusters/applications/schedule_installation_service.rb17
-rw-r--r--app/services/concerns/exclusive_lease_guard.rb52
-rw-r--r--app/services/concerns/users/participable_service.rb41
-rw-r--r--app/services/git_push_service.rb8
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/move_service.rb2
-rw-r--r--app/services/issues/reopen_service.rb2
-rw-r--r--app/services/issues/update_service.rb6
-rw-r--r--app/services/keys/base_service.rb2
-rw-r--r--app/services/keys/destroy_service.rb12
-rw-r--r--app/services/labels/transfer_service.rb11
-rw-r--r--app/services/lfs/unlock_file_service.rb8
-rw-r--r--app/services/merge_requests/close_service.rb2
-rw-r--r--app/services/merge_requests/merge_service.rb29
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb6
-rw-r--r--app/services/merge_requests/refresh_service.rb8
-rw-r--r--app/services/merge_requests/reopen_service.rb2
-rw-r--r--app/services/merge_requests/resolved_discussion_notification_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb11
-rw-r--r--app/services/milestones/base_service.rb1
-rw-r--r--app/services/notes/resolve_service.rb9
-rw-r--r--app/services/notification_recipient_service.rb60
-rw-r--r--app/services/notification_service.rb93
-rw-r--r--app/services/projects/create_from_template_service.rb3
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb27
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb6
-rw-r--r--app/services/projects/open_issues_count_service.rb38
-rw-r--r--app/services/projects/participants_service.rb32
-rw-r--r--app/services/projects/transfer_service.rb2
-rw-r--r--app/services/projects/update_pages_service.rb29
-rw-r--r--app/services/projects/update_remote_mirror_service.rb30
-rw-r--r--app/services/quick_actions/interpret_service.rb26
-rw-r--r--app/services/repository_archive_clean_up_service.rb5
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb4
-rw-r--r--app/services/users/respond_to_terms_service.rb24
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/uploaders/gitlab_uploader.rb4
-rw-r--r--app/uploaders/job_artifact_uploader.rb8
-rw-r--r--app/uploaders/object_storage.rb23
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml2
-rw-r--r--app/views/abuse_reports/new.html.haml12
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml6
-rw-r--r--app/views/admin/abuse_reports/index.html.haml2
-rw-r--r--app/views/admin/appearances/_form.html.haml24
-rw-r--r--app/views/admin/application_settings/_abuse.html.haml8
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml34
-rw-r--r--app/views/admin/application_settings/_background_jobs.html.haml22
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml42
-rw-r--r--app/views/admin/application_settings/_email.html.haml18
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml20
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml20
-rw-r--r--app/views/admin/application_settings/_influx.html.haml48
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml50
-rw-r--r--app/views/admin/application_settings/_koding.html.haml16
-rw-r--r--app/views/admin/application_settings/_logging.html.haml26
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml8
-rw-r--r--app/views/admin/application_settings/_pages.html.haml16
-rw-r--r--app/views/admin/application_settings/_performance.html.haml10
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml12
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml14
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml10
-rw-r--r--app/views/admin/application_settings/_realtime.html.haml8
-rw-r--r--app/views/admin/application_settings/_registry.html.haml6
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml48
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml16
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml48
-rw-r--r--app/views/admin/application_settings/_signin.html.haml50
-rw-r--r--app/views/admin/application_settings/_signup.html.haml52
-rw-r--r--app/views/admin/application_settings/_spam.html.haml54
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml8
-rw-r--r--app/views/admin/application_settings/_terms.html.haml22
-rw-r--r--app/views/admin/application_settings/_usage.html.haml18
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml50
-rw-r--r--app/views/admin/application_settings/show.html.haml122
-rw-r--r--app/views/admin/applications/_form.html.haml18
-rw-r--r--app/views/admin/applications/show.html.haml2
-rw-r--r--app/views/admin/background_jobs/show.html.haml8
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml26
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml4
-rw-r--r--app/views/admin/conversational_development_index/_card.html.haml20
-rw-r--r--app/views/admin/conversational_development_index/_disabled.html.haml2
-rw-r--r--app/views/admin/conversational_development_index/_no_data.html.haml2
-rw-r--r--app/views/admin/conversational_development_index/show.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml87
-rw-r--r--app/views/admin/deploy_keys/edit.html.haml2
-rw-r--r--app/views/admin/deploy_keys/index.html.haml4
-rw-r--r--app/views/admin/deploy_keys/new.html.haml2
-rw-r--r--app/views/admin/gitaly_servers/index.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml14
-rw-r--r--app/views/admin/groups/_group.html.haml4
-rw-r--r--app/views/admin/groups/show.html.haml48
-rw-r--r--app/views/admin/health_check/show.html.haml8
-rw-r--r--app/views/admin/hook_logs/_index.html.haml4
-rw-r--r--app/views/admin/hook_logs/show.html.haml2
-rw-r--r--app/views/admin/hooks/_form.html.haml14
-rw-r--r--app/views/admin/hooks/edit.html.haml4
-rw-r--r--app/views/admin/hooks/index.html.haml8
-rw-r--r--app/views/admin/identities/_form.html.haml10
-rw-r--r--app/views/admin/identities/_identity.html.haml4
-rw-r--r--app/views/admin/identities/index.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml19
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/labels/destroy.js.haml2
-rw-r--r--app/views/admin/labels/index.html.haml4
-rw-r--r--app/views/admin/logs/show.html.haml4
-rw-r--r--app/views/admin/projects/_projects.html.haml4
-rw-r--r--app/views/admin/projects/index.html.haml4
-rw-r--r--app/views/admin/projects/show.html.haml58
-rw-r--r--app/views/admin/requests_profiles/index.html.haml4
-rw-r--r--app/views/admin/runners/_runner.html.haml18
-rw-r--r--app/views/admin/runners/index.html.haml19
-rw-r--r--app/views/admin/runners/show.html.haml17
-rw-r--r--app/views/admin/services/_form.html.haml2
-rw-r--r--app/views/admin/services/index.html.haml3
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml12
-rw-r--r--app/views/admin/system_info/show.html.haml8
-rw-r--r--app/views/admin/users/_access_levels.html.haml8
-rw-r--r--app/views/admin/users/_form.html.haml46
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/admin/users/_profile.html.haml4
-rw-r--r--app/views/admin/users/_projects.html.haml8
-rw-r--r--app/views/admin/users/_user.html.haml16
-rw-r--r--app/views/admin/users/index.html.haml18
-rw-r--r--app/views/admin/users/projects.html.haml20
-rw-r--r--app/views/admin/users/show.html.haml56
-rw-r--r--app/views/ci/lints/show.html.haml4
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml6
-rw-r--r--app/views/ci/variables/_variable_row.html.haml6
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml4
-rw-r--r--app/views/dashboard/projects/_nav.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml12
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.haml2
-rw-r--r--app/views/devise/mailer/unlock_instructions.text.erb2
-rw-r--r--app/views/devise/sessions/_new_base.html.haml4
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml2
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_tab_single.html.haml6
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml18
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml10
-rw-r--r--app/views/discussions/_discussion.html.haml4
-rw-r--r--app/views/discussions/_notes.html.haml4
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml4
-rw-r--r--app/views/doorkeeper/applications/index.html.haml2
-rw-r--r--app/views/doorkeeper/applications/show.html.haml2
-rw-r--r--app/views/doorkeeper/authorized_applications/index.html.haml2
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/explore/groups/_nav.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml2
-rw-r--r--app/views/explore/projects/_nav.html.haml2
-rw-r--r--app/views/groups/_create_chat_team.html.haml4
-rw-r--r--app/views/groups/_group_admin_settings.html.haml52
-rw-r--r--app/views/groups/edit.html.haml36
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml6
-rw-r--r--app/views/groups/group_members/index.html.haml28
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/milestones/_form.html.haml10
-rw-r--r--app/views/groups/new.html.haml10
-rw-r--r--app/views/groups/projects.html.haml10
-rw-r--r--app/views/groups/runners/_group_runners.html.haml24
-rw-r--r--app/views/groups/runners/_index.html.haml9
-rw-r--r--app/views/groups/runners/_runner.html.haml27
-rw-r--r--app/views/groups/runners/edit.html.haml6
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml28
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml27
-rw-r--r--app/views/help/index.html.haml6
-rw-r--r--app/views/help/ui.html.haml71
-rw-r--r--app/views/ide/index.html.haml3
-rw-r--r--app/views/import/_githubish_status.html.haml9
-rw-r--r--app/views/import/bitbucket/status.html.haml11
-rw-r--r--app/views/import/fogbugz/new.html.haml14
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml2
-rw-r--r--app/views/import/gitea/new.html.haml10
-rw-r--r--app/views/import/gitlab_projects/new.html.haml14
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/new_user_map.html.haml4
-rw-r--r--app/views/import/google_code/status.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_first_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_gap.html.haml5
-rw-r--r--app/views/kaminari/gitlab/_last_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_next_page.html.haml11
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_prev_page.html.haml11
-rw-r--r--app/views/kaminari/gitlab/_without_count.html.haml8
-rw-r--r--app/views/layouts/_flash.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml3
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml15
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml6
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml22
-rw-r--r--app/views/layouts/header/_default.html.haml45
-rw-r--r--app/views/layouts/header/_empty.html.haml4
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml16
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml8
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml11
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml134
-rw-r--r--app/views/layouts/terms.html.haml34
-rw-r--r--app/views/notify/member_invited_email.html.haml2
-rw-r--r--app/views/notify/merge_request_unmergeable_email.html.haml6
-rw-r--r--app/views/notify/merge_request_unmergeable_email.text.haml11
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml4
-rw-r--r--app/views/notify/pipeline_success_email.html.haml4
-rw-r--r--app/views/peek/_bar.html.haml7
-rw-r--r--app/views/profiles/_event_table.html.haml2
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml31
-rw-r--r--app/views/profiles/active_sessions/index.html.haml15
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml2
-rw-r--r--app/views/profiles/chat_names/new.html.haml2
-rw-r--r--app/views/profiles/emails/index.html.haml14
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml6
-rw-r--r--app/views/profiles/keys/_key.html.haml8
-rw-r--r--app/views/profiles/keys/_key_details.html.haml6
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml2
-rw-r--r--app/views/profiles/passwords/edit.html.haml2
-rw-r--r--app/views/profiles/passwords/new.html.haml14
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml10
-rw-r--r--app/views/profiles/preferences/show.html.haml4
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_import_project_pane.html.haml51
-rw-r--r--app/views/projects/_issuable_by_email.html.haml4
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_merge_request_fast_forward_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml8
-rw-r--r--app/views/projects/_merge_request_rebase_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_settings.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml12
-rw-r--r--app/views/projects/_new_project_push_tip.html.haml6
-rw-r--r--app/views/projects/_project_templates.html.haml11
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml6
-rw-r--r--app/views/projects/_wiki.html.haml2
-rw-r--r--app/views/projects/artifacts/file.html.haml6
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_breadcrumb.html.haml4
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/_header_content.html.haml2
-rw-r--r--app/views/projects/blob/_new_dir.html.haml6
-rw-r--r--app/views/projects/blob/_remove.html.haml6
-rw-r--r--app/views/projects/blob/_upload.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml15
-rw-r--r--app/views/projects/branches/_panel.html.haml8
-rw-r--r--app/views/projects/branches/index.html.haml4
-rw-r--r--app/views/projects/branches/new.html.haml14
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml10
-rw-r--r--app/views/projects/ci/lints/show.html.haml23
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml2
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml4
-rw-r--r--app/views/projects/clusters/_gcp_signup_offer_banner.html.haml12
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml16
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml4
-rw-r--r--app/views/projects/clusters/index.html.haml2
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/clusters/user/_show.html.haml7
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml2
-rw-r--r--app/views/projects/commit/_change.html.haml6
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml6
-rw-r--r--app/views/projects/commit/_commit_box.html.haml12
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/branches.html.haml3
-rw-r--r--app/views/projects/commits/_commit.html.haml18
-rw-r--r--app/views/projects/commits/_commit_list.html.haml4
-rw-r--r--app/views/projects/commits/_inline_commit.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml4
-rw-r--r--app/views/projects/compare/_form.html.haml53
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml14
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml22
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/deploy_keys/edit.html.haml2
-rw-r--r--app/views/projects/deploy_tokens/_revoke_modal.html.haml2
-rw-r--r--app/views/projects/deploy_tokens/_table.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml10
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/diffs/_file_header.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml4
-rw-r--r--app/views/projects/diffs/_warning.html.haml2
-rw-r--r--app/views/projects/edit.html.haml9
-rw-r--r--app/views/projects/empty.html.haml26
-rw-r--r--app/views/projects/find_file/show.html.haml4
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/forks/new.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml4
-rw-r--r--app/views/projects/graphs/charts.html.haml2
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/hook_logs/_index.html.haml4
-rw-r--r--app/views/projects/hook_logs/show.html.haml2
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/projects/imports/new.html.haml8
-rw-r--r--app/views/projects/issues/_form.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml12
-rw-r--r--app/views/projects/issues/_new_branch.html.haml8
-rw-r--r--app/views/projects/issues/show.html.haml18
-rw-r--r--app/views/projects/jobs/_empty_state.html.haml4
-rw-r--r--app/views/projects/jobs/_header.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml17
-rw-r--r--app/views/projects/jobs/_user.html.haml4
-rw-r--r--app/views/projects/jobs/index.html.haml2
-rw-r--r--app/views/projects/jobs/show.html.haml6
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml2
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml6
-rw-r--r--app/views/projects/mattermosts/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml18
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml12
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml4
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml26
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml54
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml12
-rw-r--r--app/views/projects/merge_requests/diffs/_commit_widget.html.haml2
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml4
-rw-r--r--app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml2
-rw-r--r--app/views/projects/merge_requests/dropdowns/_project.html.haml2
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml8
-rw-r--r--app/views/projects/merge_requests/show.html.haml12
-rw-r--r--app/views/projects/milestones/_form.html.haml10
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml10
-rw-r--r--app/views/projects/mirrors/_push.html.haml50
-rw-r--r--app/views/projects/mirrors/_show.html.haml3
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml69
-rw-r--r--app/views/projects/no_repo.html.haml2
-rw-r--r--app/views/projects/pages/_access.html.haml6
-rw-r--r--app/views/projects/pages/_destroy.haml6
-rw-r--r--app/views/projects/pages/_https_only.html.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml8
-rw-r--r--app/views/projects/pages/_no_domains.html.haml4
-rw-r--r--app/views/projects/pages/_use.html.haml6
-rw-r--r--app/views/projects/pages/show.html.haml2
-rw-r--r--app/views/projects/pages_domains/_form.html.haml12
-rw-r--r--app/views/projects/pages_domains/edit.html.haml2
-rw-r--r--app/views/projects/pages_domains/new.html.haml4
-rw-r--r--app/views/projects/pages_domains/show.html.haml18
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml14
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_tabs.html.haml8
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml4
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml17
-rw-r--r--app/views/projects/pipelines/new.html.haml24
-rw-r--r--app/views/projects/project_members/_groups.html.haml6
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/_new_shared_group.html.haml2
-rw-r--r--app/views/projects/project_members/_team.html.haml6
-rw-r--r--app/views/projects/project_members/import.html.haml6
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml57
-rw-r--r--app/views/projects/protected_branches/shared/_matching_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml49
-rw-r--r--app/views/projects/protected_tags/shared/_matching_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_protected_tag.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_tags_list.html.haml4
-rw-r--r--app/views/projects/registry/repositories/_tag.html.haml4
-rw-r--r--app/views/projects/registry/repositories/index.html.haml12
-rw-r--r--app/views/projects/releases/edit.html.haml2
-rw-r--r--app/views/projects/repositories/_feed.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml55
-rw-r--r--app/views/projects/runners/_group_runners.html.haml37
-rw-r--r--app/views/projects/runners/_index.html.haml18
-rw-r--r--app/views/projects/runners/_runner.html.haml22
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml6
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml3
-rw-r--r--app/views/projects/runners/edit.html.haml4
-rw-r--r--app/views/projects/runners/show.html.haml67
-rw-r--r--app/views/projects/services/_form.html.haml2
-rw-r--r--app/views/projects/services/_index.html.haml9
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml82
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_help.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml20
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml68
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml43
-rw-r--r--app/views/projects/settings/ci_cd/_badge.html.haml8
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml66
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml14
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/projects/snippets/_actions.html.haml4
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/new.html.haml24
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_blob_item.html.haml4
-rw-r--r--app/views/projects/tree/_submodule_item.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml6
-rw-r--r--app/views/projects/tree/_tree_header.html.haml12
-rw-r--r--app/views/projects/tree/_tree_item.html.haml2
-rw-r--r--app/views/projects/triggers/_content.html.haml2
-rw-r--r--app/views/projects/triggers/_form.html.haml2
-rw-r--r--app/views/projects/triggers/_index.html.haml10
-rw-r--r--app/views/projects/triggers/_trigger.html.haml6
-rw-r--r--app/views/projects/wikis/_form.html.haml16
-rw-r--r--app/views/projects/wikis/_pages_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml11
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml2
-rw-r--r--app/views/search/_category.html.haml28
-rw-r--r--app/views/search/_filter.html.haml4
-rw-r--r--app/views/search/results/_issue.html.haml4
-rw-r--r--app/views/search/results/_merge_request.html.haml6
-rw-r--r--app/views/search/results/_snippet_title.html.haml6
-rw-r--r--app/views/shared/_allow_request_access.html.haml2
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml4
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml10
-rw-r--r--app/views/shared/_commit_message_container.html.haml4
-rw-r--r--app/views/shared/_commit_well.html.haml2
-rw-r--r--app/views/shared/_confirm_modal.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_field.html.haml6
-rw-r--r--app/views/shared/_group_form.html.haml28
-rw-r--r--app/views/shared/_import_form.html.haml4
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml8
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_milestones_filter.html.haml8
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml3
-rw-r--r--app/views/shared/_new_commit_form.html.haml4
-rw-r--r--app/views/shared/_new_merge_request_checkbox.html.haml2
-rw-r--r--app/views/shared/_personal_access_tokens_table.html.haml2
-rw-r--r--app/views/shared/_project_limit.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml13
-rw-r--r--app/views/shared/_service_settings.html.haml12
-rw-r--r--app/views/shared/_sort_dropdown.html.haml4
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/_visibility_radios.html.haml2
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml9
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml4
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml6
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml2
-rw-r--r--app/views/shared/builds/_tabs.html.haml10
-rw-r--r--app/views/shared/deploy_keys/_form.html.haml8
-rw-r--r--app/views/shared/empty_states/_issues.html.haml4
-rw-r--r--app/views/shared/empty_states/_labels.html.haml4
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml4
-rw-r--r--app/views/shared/form_elements/_description.html.haml4
-rw-r--r--app/views/shared/groups/_dropdown.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/hook_logs/_content.html.haml4
-rw-r--r--app/views/shared/hook_logs/_status_label.html.haml2
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml6
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml6
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml16
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml4
-rw-r--r--app/views/shared/issuable/_nav.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml29
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml8
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml8
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml8
-rw-r--r--app/views/shared/issuable/form/_contribution.html.haml6
-rw-r--r--app/views/shared/issuable/form/_issue_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml6
-rw-r--r--app/views/shared/issuable/form/_merge_request_assignee.html.haml6
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml22
-rw-r--r--app/views/shared/issuable/form/_metadata_issue_assignee.html.haml4
-rw-r--r--app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_title.html.haml4
-rw-r--r--app/views/shared/labels/_form.html.haml19
-rw-r--r--app/views/shared/members/_filter_2fa_dropdown.html.haml11
-rw-r--r--app/views/shared/members/_group.html.haml8
-rw-r--r--app/views/shared/members/_member.html.haml22
-rw-r--r--app/views/shared/members/_requests.html.haml6
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml13
-rw-r--r--app/views/shared/milestones/_issuables.html.haml6
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml18
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml49
-rw-r--r--app/views/shared/milestones/_tabs.html.haml12
-rw-r--r--app/views/shared/milestones/_top.html.haml4
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--app/views/shared/notes/_hints.html.haml2
-rw-r--r--app/views/shared/notes/_note.html.haml7
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/plugins/_index.html.haml6
-rw-r--r--app/views/shared/projects/_dropdown.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/runners/_form.html.haml56
-rw-r--r--app/views/shared/runners/_runner_description.html.haml16
-rw-r--r--app/views/shared/runners/show.html.haml71
-rw-r--r--app/views/shared/snippets/_blob.html.haml2
-rw-r--r--app/views/shared/snippets/_embed.html.haml2
-rw-r--r--app/views/shared/snippets/_form.html.haml10
-rw-r--r--app/views/shared/snippets/_header.html.haml4
-rw-r--r--app/views/shared/snippets/_snippet.html.haml8
-rw-r--r--app/views/shared/web_hooks/_form.html.haml24
-rw-r--r--app/views/shared/web_hooks/_test_button.html.haml2
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/sherlock/queries/_backtrace.html.haml8
-rw-r--r--app/views/sherlock/queries/_general.html.haml20
-rw-r--r--app/views/sherlock/queries/show.html.haml4
-rw-r--r--app/views/sherlock/transactions/_file_samples.html.haml2
-rw-r--r--app/views/sherlock/transactions/_general.html.haml7
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml2
-rw-r--r--app/views/sherlock/transactions/index.html.haml7
-rw-r--r--app/views/sherlock/transactions/show.html.haml8
-rw-r--r--app/views/snippets/_actions.html.haml4
-rw-r--r--app/views/snippets/_snippets_scope_menu.html.haml10
-rw-r--r--app/views/snippets/index.html.haml6
-rw-r--r--app/views/users/show.html.haml14
-rw-r--r--app/views/users/terms/index.html.haml13
-rw-r--r--app/workers/admin_email_worker.rb6
-rw-r--r--app/workers/all_queues.yml5
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb12
-rw-r--r--app/workers/concerns/mail_scheduler_queue.rb4
-rw-r--r--app/workers/create_note_diff_file_worker.rb9
-rw-r--r--app/workers/gitlab/github_import/advance_stage_worker.rb9
-rw-r--r--app/workers/gitlab/github_import/refresh_import_jid_worker.rb5
-rw-r--r--app/workers/mail_scheduler/issue_due_worker.rb2
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb19
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb79
-rw-r--r--app/workers/repository_check/batch_worker.rb35
-rw-r--r--app/workers/repository_check/single_repository_worker.rb48
-rw-r--r--app/workers/repository_fork_worker.rb4
-rw-r--r--app/workers/repository_remove_remote_worker.rb35
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb49
-rw-r--r--app/workers/stuck_import_jobs_worker.rb9
-rwxr-xr-xbin/secpick5
-rwxr-xr-xbin/spinach13
-rw-r--r--changelogs/unreleased/16957-issue-due-email.yml5
-rw-r--r--changelogs/unreleased/17516-nested-restore-changelog.yml5
-rw-r--r--changelogs/unreleased/17939-osw-patch-support-gfm.yml5
-rw-r--r--changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml5
-rw-r--r--changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml5
-rw-r--r--changelogs/unreleased/20394-protected-branches-wildcard.yml5
-rw-r--r--changelogs/unreleased/21677-run-pipeline-word.yml5
-rw-r--r--changelogs/unreleased/22647-width-contributors-graphs.yml5
-rw-r--r--changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml6
-rw-r--r--changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml5
-rw-r--r--changelogs/unreleased/23465-print-markdown.yml5
-rw-r--r--changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml5
-rw-r--r--changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml5
-rw-r--r--changelogs/unreleased/31114-internal-ids-are-not-atomic.yml5
-rw-r--r--changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml5
-rw-r--r--changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml6
-rw-r--r--changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml5
-rw-r--r--changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml5
-rw-r--r--changelogs/unreleased/35475-lazy-diff.yml5
-rw-r--r--changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml5
-rw-r--r--changelogs/unreleased/39710-search-placeholder-cut-off.yml5
-rw-r--r--changelogs/unreleased/39880-merge-method-api.yml5
-rw-r--r--changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml5
-rw-r--r--changelogs/unreleased/40487-axios-pipelines.yml4
-rw-r--r--changelogs/unreleased/40725-move-mr-external-link-to-right.yml5
-rw-r--r--changelogs/unreleased/40781-os-to-ce.yml5
-rw-r--r--changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml5
-rw-r--r--changelogs/unreleased/41224-pipeline-icons.yml5
-rw-r--r--changelogs/unreleased/41436-use-simpler-env-vars-for-auto-devops-replicas.yml5
-rw-r--r--changelogs/unreleased/41748-vertical-misalignment-login-box.yml5
-rw-r--r--changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml5
-rw-r--r--changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml5
-rw-r--r--changelogs/unreleased/41967_issue_api_closed_by_info.yml5
-rw-r--r--changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml5
-rw-r--r--changelogs/unreleased/42028-xss-diffs.yml5
-rw-r--r--changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml5
-rw-r--r--changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml6
-rw-r--r--changelogs/unreleased/42531-open-invite-404.yml5
-rw-r--r--changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml5
-rw-r--r--changelogs/unreleased/42568-pipeline-empty-state.yml5
-rw-r--r--changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml5
-rw-r--r--changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml5
-rw-r--r--changelogs/unreleased/42889-avoid-return-inside-block.yml5
-rw-r--r--changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml5
-rw-r--r--changelogs/unreleased/43215-update-design-for-verifying-domains.yml5
-rw-r--r--changelogs/unreleased/43246-checkfilter.yml6
-rw-r--r--changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml5
-rw-r--r--changelogs/unreleased/43367-fix-board-long-strings.yml5
-rw-r--r--changelogs/unreleased/43404-pipelines-commit.yml5
-rw-r--r--changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml5
-rw-r--r--changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml5
-rw-r--r--changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml5
-rw-r--r--changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml5
-rw-r--r--changelogs/unreleased/43567-replace-gke.yml5
-rw-r--r--changelogs/unreleased/43603-ci-lint-support.yml5
-rw-r--r--changelogs/unreleased/43617-mailsig.yml5
-rw-r--r--changelogs/unreleased/43673-operations-tab-mvc.yml5
-rw-r--r--changelogs/unreleased/43702-update-label-dropdown-wording.yml5
-rw-r--r--changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml5
-rw-r--r--changelogs/unreleased/43720-update-fe-webpack-docs.yml6
-rw-r--r--changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml5
-rw-r--r--changelogs/unreleased/43771-improve-avatar-error-message.yml5
-rw-r--r--changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml5
-rw-r--r--changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml5
-rw-r--r--changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml5
-rw-r--r--changelogs/unreleased/43933-always-notify-mentions.yml6
-rw-r--r--changelogs/unreleased/43949-verify-job-artifacts.yml5
-rw-r--r--changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml5
-rw-r--r--changelogs/unreleased/44022-singular-1-diff.yml5
-rw-r--r--changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml6
-rw-r--r--changelogs/unreleased/44160-update-foreman-to-0-84-0.yml5
-rw-r--r--changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml5
-rw-r--r--changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml5
-rw-r--r--changelogs/unreleased/44224-remove-gl.yml5
-rw-r--r--changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml5
-rw-r--r--changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml5
-rw-r--r--changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml5
-rw-r--r--changelogs/unreleased/44280-fix-code-search.yml5
-rw-r--r--changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml5
-rw-r--r--changelogs/unreleased/44296-commit-path.yml6
-rw-r--r--changelogs/unreleased/44319-remove-gray-buttons.yml5
-rw-r--r--changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml5
-rw-r--r--changelogs/unreleased/44383-cleanup-framework-header.yml5
-rw-r--r--changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml5
-rw-r--r--changelogs/unreleased/44386-better-ux-for-long-name-branches.yml5
-rw-r--r--changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml5
-rw-r--r--changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml5
-rw-r--r--changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml5
-rw-r--r--changelogs/unreleased/44425-use-gitlab_environment.yml5
-rw-r--r--changelogs/unreleased/44508-fix-fork-namespace-images.yml5
-rw-r--r--changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml5
-rw-r--r--changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml5
-rw-r--r--changelogs/unreleased/44582-clear-pipeline-status-cache.yml5
-rw-r--r--changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml5
-rw-r--r--changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml5
-rw-r--r--changelogs/unreleased/44697-prevue.yml5
-rw-r--r--changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml5
-rw-r--r--changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml5
-rw-r--r--changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml5
-rw-r--r--changelogs/unreleased/44799-api-naming-issue-scope.yml5
-rw-r--r--changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml5
-rw-r--r--changelogs/unreleased/44902-remove-rake-test-ci.yml5
-rw-r--r--changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml5
-rw-r--r--changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml5
-rw-r--r--changelogs/unreleased/45065-users-projects-json-sort.yml5
-rw-r--r--changelogs/unreleased/45159-fix-illustration.yml5
-rw-r--r--changelogs/unreleased/45190-create-notes-diff-files.yml5
-rw-r--r--changelogs/unreleased/45271-collpased-diff-loading.yml5
-rw-r--r--changelogs/unreleased/45287-align-icons.yml5
-rw-r--r--changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml6
-rw-r--r--changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml5
-rw-r--r--changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml5
-rw-r--r--changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml5
-rw-r--r--changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml5
-rw-r--r--changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml5
-rw-r--r--changelogs/unreleased/45715-remove-modal-retry.yml5
-rw-r--r--changelogs/unreleased/45827-expose_readme_url_in_project_api.yml5
-rw-r--r--changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml5
-rw-r--r--changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml5
-rw-r--r--changelogs/unreleased/46010-add-index-to-runner-type.yml5
-rw-r--r--changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml5
-rw-r--r--changelogs/unreleased/46193-fix-big-estimate.yml5
-rw-r--r--changelogs/unreleased/46354-deprecate_gemnasium_service.yml5
-rw-r--r--changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml5
-rw-r--r--changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml5
-rw-r--r--changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml5
-rw-r--r--changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml5
-rw-r--r--changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml6
-rw-r--r--changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml5
-rw-r--r--changelogs/unreleased/8088_embedded_snippets_support.yml5
-rw-r--r--changelogs/unreleased/Link_to_project_labels_page.yml5
-rw-r--r--changelogs/unreleased/ab-37125-assigned-issues-query.yml5
-rw-r--r--changelogs/unreleased/ab-37462-cache-personal-projects-count.yml5
-rw-r--r--changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml5
-rw-r--r--changelogs/unreleased/ab-43706-composite-primary-keys.yml5
-rw-r--r--changelogs/unreleased/ab-44467-remove-index.yml5
-rw-r--r--changelogs/unreleased/ab-45247-project-lookups-validation.yml5
-rw-r--r--changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml5
-rw-r--r--changelogs/unreleased/ac-fix-use_file-race.yml5
-rw-r--r--changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml5
-rw-r--r--changelogs/unreleased/ac-pages-port.yml5
-rw-r--r--changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml5
-rw-r--r--changelogs/unreleased/add-canary-favicon.yml5
-rw-r--r--changelogs/unreleased/add-cpu-mem-totals.yml5
-rw-r--r--changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml5
-rw-r--r--changelogs/unreleased/add-per-runner-job-timeout.yml5
-rw-r--r--changelogs/unreleased/add-query-counts-to-profiler-output.yml5
-rw-r--r--changelogs/unreleased/ajax-requests-in-performance-bar.yml5
-rw-r--r--changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-remove-spinach.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml5
-rw-r--r--changelogs/unreleased/bvl-add-username-to-terms-message.yml5
-rw-r--r--changelogs/unreleased/bvl-export-import-lfs.yml5
-rw-r--r--changelogs/unreleased/bvl-import-zip-multiple-assignees.yml5
-rw-r--r--changelogs/unreleased/bvl-no-permanent-redirect.yml5
-rw-r--r--changelogs/unreleased/bvl-override-import-params.yml5
-rw-r--r--changelogs/unreleased/ccr-incoming-email-regex-anchor.yml3
-rw-r--r--changelogs/unreleased/ci-pipeline-commit-lookup.yml5
-rw-r--r--changelogs/unreleased/collapsed-contextual-nav-update.yml6
-rw-r--r--changelogs/unreleased/commit-branch-tag-icon-update.yml5
-rw-r--r--changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml5
-rw-r--r--changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml5
-rw-r--r--changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/deploy-tokens-container-registry-specs.yml5
-rw-r--r--changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml5
-rw-r--r--changelogs/unreleased/direct-upload-of-uploads.yml5
-rw-r--r--changelogs/unreleased/dm-deploy-keys-default-user.yml5
-rw-r--r--changelogs/unreleased/dm-flatten-tree-plus-chars.yml5
-rw-r--r--changelogs/unreleased/dm-internal-user-namespace.yml5
-rw-r--r--changelogs/unreleased/docs-42067-document-runner-registration-api.yml5
-rw-r--r--changelogs/unreleased/docs-for-failure-reason-tooltip.yml5
-rw-r--r--changelogs/unreleased/dz-add-2fa-filter.yml5
-rw-r--r--changelogs/unreleased/dz-improve-app-settings-2.yml5
-rw-r--r--changelogs/unreleased/escape-autocomplete-values-for-markdown.yml5
-rw-r--r--changelogs/unreleased/expose-commits-mr-api.yml5
-rw-r--r--changelogs/unreleased/feature-add-language-in-repository-to-api.yml5
-rw-r--r--changelogs/unreleased/feature-add_target_to_tags.yml5
-rw-r--r--changelogs/unreleased/feature-expose-runner-ip-to-api.yml5
-rw-r--r--changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml5
-rw-r--r--changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml5
-rw-r--r--changelogs/unreleased/feature_detect_co_authored_commits.yml6
-rw-r--r--changelogs/unreleased/fix-40798-namespace-forking.yml5
-rw-r--r--changelogs/unreleased/fix-42459---in-branch.yml5
-rw-r--r--changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml6
-rw-r--r--changelogs/unreleased/fix-assignee-name-wrap.yml5
-rw-r--r--changelogs/unreleased/fix-auth0-unsafe-login.yml5
-rw-r--r--changelogs/unreleased/fix-dashboard-sorting.yml5
-rw-r--r--changelogs/unreleased/fix-devops-remove-beta.yml5
-rw-r--r--changelogs/unreleased/fix-emoji-popup.yml5
-rw-r--r--changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml5
-rw-r--r--changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml5
-rw-r--r--changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml5
-rw-r--r--changelogs/unreleased/fix-mattermost-delete-team.yml5
-rw-r--r--changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml5
-rw-r--r--changelogs/unreleased/fix-projects-no-repository-placeholder.yml5
-rw-r--r--changelogs/unreleased/fix-reactive-cache-retry-rate.yml5
-rw-r--r--changelogs/unreleased/fix-references-in-group-context.yml5
-rw-r--r--changelogs/unreleased/fix-registry-created-at-tooltip.yml5
-rw-r--r--changelogs/unreleased/fix-shorcut-modal.yml5
-rw-r--r--changelogs/unreleased/fix-unverified-hover-state.yml5
-rw-r--r--changelogs/unreleased/fix-wiki-find-file-gitaly.yml5
-rw-r--r--changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml5
-rw-r--r--changelogs/unreleased/fj-174-better-ldap-connection-handling.yml5
-rw-r--r--changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml5
-rw-r--r--changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml5
-rw-r--r--changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml5
-rw-r--r--changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml5
-rw-r--r--changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml5
-rw-r--r--changelogs/unreleased/fl-fix-annoying-actions.yml5
-rw-r--r--changelogs/unreleased/fl-pipelines-details-axios.yml5
-rw-r--r--changelogs/unreleased/ide-file-row-hover-style.yml5
-rw-r--r--changelogs/unreleased/ide-folder-button-path.yml5
-rw-r--r--changelogs/unreleased/ide-hide-merge-request-if-disabled.yml5
-rw-r--r--changelogs/unreleased/ide-mr-changes-alert-box.yml5
-rw-r--r--changelogs/unreleased/ide-project-avatar-identicon.yml5
-rw-r--r--changelogs/unreleased/ide-subgroup-fix.yml5
-rw-r--r--changelogs/unreleased/ide-tree-loading-fix.yml5
-rw-r--r--changelogs/unreleased/improve-jobs-queuing-time-metric.yml5
-rw-r--r--changelogs/unreleased/increase-unicorn-memory-killer-limits.yml5
-rw-r--r--changelogs/unreleased/issue-25256.yml5
-rw-r--r--changelogs/unreleased/issue_25542.yml5
-rw-r--r--changelogs/unreleased/issue_38418.yml5
-rw-r--r--changelogs/unreleased/issue_40915.yml5
-rw-r--r--changelogs/unreleased/issue_42443.yml5
-rw-r--r--changelogs/unreleased/issue_44270.yml5
-rw-r--r--changelogs/unreleased/jej-commit-api-tracks-lfs.yml5
-rw-r--r--changelogs/unreleased/jej-mattermost-notification-confidentiality.yml5
-rw-r--r--changelogs/unreleased/jivl-add-dot-system-notes.yml5
-rw-r--r--changelogs/unreleased/jivl-realtime-update-adding-file.yml5
-rw-r--r--changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml5
-rw-r--r--changelogs/unreleased/jprovazn-fix-resolvable.yml5
-rw-r--r--changelogs/unreleased/jprovazn-issueref.yml6
-rw-r--r--changelogs/unreleased/jprovazn-null-byte.yml5
-rw-r--r--changelogs/unreleased/jprovazn-pipeline-policy.yml6
-rw-r--r--changelogs/unreleased/jprovazn-remote-upload-destroy.yml5
-rw-r--r--changelogs/unreleased/jr-web-ide-shortcuts.yml5
-rw-r--r--changelogs/unreleased/jramsay-38830-tarball.yml5
-rw-r--r--changelogs/unreleased/memoize-database-version.yml5
-rw-r--r--changelogs/unreleased/merge-request-widget-source-branch-improvements.yml6
-rw-r--r--changelogs/unreleased/migrate-restore-repo-to-gitaly.yml5
-rw-r--r--changelogs/unreleased/move-board-blank-state-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-disussion-actions-to-the-right.yml5
-rw-r--r--changelogs/unreleased/move-email-footer-info-to-single-line.yml5
-rw-r--r--changelogs/unreleased/move-estimate-only-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-help-state-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-pipeline-failed-vue-component.yml5
-rw-r--r--changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml5
-rw-r--r--changelogs/unreleased/mr-conflict-notification.yml5
-rw-r--r--changelogs/unreleased/optional-api-delimiter.yml5
-rw-r--r--changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml5
-rw-r--r--changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml5
-rw-r--r--changelogs/unreleased/pages_force_https.yml5
-rw-r--r--changelogs/unreleased/pipelines-index-performance.yml5
-rw-r--r--changelogs/unreleased/poc-upload-hashing-path.yml5
-rw-r--r--changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml5
-rw-r--r--changelogs/unreleased/refactor-move-assignee-title-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-time-tracking-vue-components.yml5
-rw-r--r--changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml5
-rw-r--r--changelogs/unreleased/remove-pages-tar-support.yml5
-rw-r--r--changelogs/unreleased/rename-merge-request-widget-author-component.yml5
-rw-r--r--changelogs/unreleased/rename-overview-project-sidenav.yml5
-rw-r--r--changelogs/unreleased/rendering-markdown-multiple-projects.yml5
-rw-r--r--changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml5
-rw-r--r--changelogs/unreleased/sh-appearance-cache-key-version.yml5
-rw-r--r--changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml5
-rw-r--r--changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml5
-rw-r--r--changelogs/unreleased/sh-fix-backup-specific-rake-task.yml5
-rw-r--r--changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml5
-rw-r--r--changelogs/unreleased/sh-fix-grape-logging-status-code.yml5
-rw-r--r--changelogs/unreleased/sh-gitlab-sidekiq-logger.yml5
-rw-r--r--changelogs/unreleased/sh-memoize-repository-empty.yml5
-rw-r--r--changelogs/unreleased/sh-move-delete-groups-api-async.yml5
-rw-r--r--changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml5
-rw-r--r--changelogs/unreleased/support-active-setting-while-registering-a-runner.yml5
-rw-r--r--changelogs/unreleased/tc-re-add-read-only-banner.yml5
-rw-r--r--changelogs/unreleased/ui-mr-counter-cache.yml5
-rw-r--r--changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml5
-rw-r--r--changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml5
-rw-r--r--changelogs/unreleased/update-unresolved-discussions-vue-component.yml5
-rw-r--r--changelogs/unreleased/update-wiki-modal.yml5
-rw-r--r--changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml5
-rw-r--r--changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml5
-rw-r--r--changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml5
-rw-r--r--changelogs/unreleased/winh-deprecate-old-modal.yml5
-rw-r--r--changelogs/unreleased/winh-dropdown-entry-unlocking.yml5
-rw-r--r--changelogs/unreleased/winh-make-it-right-now.yml5
-rw-r--r--changelogs/unreleased/winh-new-merge-request-encoding.yml5
-rw-r--r--changelogs/unreleased/workhorse-gitaly-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-add-branch-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-branch-containing-sha-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-bump-gitaly.yml5
-rw-r--r--changelogs/unreleased/zj-calculate-checksum-mandator.yml5
-rw-r--r--changelogs/unreleased/zj-feature-gate-remove-http-api.yml5
-rw-r--r--changelogs/unreleased/zj-find-license-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-opt-out-delete-refs.yml5
-rw-r--r--changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml5
-rw-r--r--changelogs/unreleased/zj-ref-contains-sha-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-ref-exists-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-remote-repo-exists.yml5
-rw-r--r--changelogs/unreleased/zj-tag-containing-sha-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-wiki-find-file-opt-out.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-archive-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-workhorse-commit-patch-diff.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example24
-rw-r--r--config/initializers/1_settings.rb135
-rw-r--r--config/initializers/2_app.rb8
-rw-r--r--config/initializers/2_gitlab.rb1
-rw-r--r--config/initializers/6_validations.rb11
-rw-r--r--config/initializers/7_prometheus_metrics.rb2
-rw-r--r--config/initializers/8_metrics.rb9
-rw-r--r--config/initializers/9_fast_gettext.rb (renamed from config/initializers/fast_gettext.rb)0
-rw-r--r--config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb98
-rw-r--r--config/initializers/console_message.rb10
-rw-r--r--config/initializers/deprecations.rb8
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/initializers/flipper.rb21
-rw-r--r--config/initializers/forbid_sidekiq_in_transactions.rb2
-rw-r--r--config/initializers/gollum.rb14
-rw-r--r--config/initializers/kubeclient.rb16
-rw-r--r--config/initializers/lograge.rb18
-rw-r--r--config/initializers/omniauth.rb1
-rw-r--r--config/initializers/pages.rb2
-rw-r--r--config/initializers/peek.rb1
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/initializers/session_store.rb26
-rw-r--r--config/initializers/static_files.rb2
-rw-r--r--config/initializers/trusted_proxies.rb13
-rw-r--r--config/initializers/warden.rb16
-rw-r--r--config/karma.config.js92
-rw-r--r--config/prometheus/additional_metrics.yml12
-rw-r--r--config/routes/group.rb7
-rw-r--r--config/routes/profile.rb1
-rw-r--r--config/routes/project.rb9
-rw-r--r--config/routes/repository.rb1
-rw-r--r--config/routes/user.rb25
-rw-r--r--config/settings.rb126
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--config/webpack.config.js298
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/migrate/20121220064453_init_schema.rb307
-rw-r--r--db/migrate/20130102143055_rename_owner_to_creator_for_project.rb6
-rw-r--r--db/migrate/20130110172407_add_public_to_project.rb6
-rw-r--r--db/migrate/20130123114545_add_issues_tracker_to_project.rb6
-rw-r--r--db/migrate/20130125090214_add_user_permissions.rb12
-rw-r--r--db/migrate/20130131070232_remove_private_flag_from_project.rb10
-rw-r--r--db/migrate/20130206084024_add_description_to_namsespace.rb6
-rw-r--r--db/migrate/20130207104426_add_description_to_teams.rb6
-rw-r--r--db/migrate/20130211085435_add_issues_tracker_id_to_project.rb6
-rw-r--r--db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb6
-rw-r--r--db/migrate/20130218140952_add_state_to_issue.rb6
-rw-r--r--db/migrate/20130218141038_add_state_to_merge_request.rb6
-rw-r--r--db/migrate/20130218141117_add_state_to_milestone.rb6
-rw-r--r--db/migrate/20130218141258_convert_closed_to_state_in_issue.rb19
-rw-r--r--db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb21
-rw-r--r--db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb19
-rw-r--r--db/migrate/20130218141444_remove_merged_from_merge_request.rb10
-rw-r--r--db/migrate/20130218141507_remove_closed_from_issue.rb10
-rw-r--r--db/migrate/20130218141536_remove_closed_from_merge_request.rb10
-rw-r--r--db/migrate/20130218141554_remove_closed_from_milestone.rb10
-rw-r--r--db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb6
-rw-r--r--db/migrate/20130220125544_convert_merge_status_in_merge_request.rb20
-rw-r--r--db/migrate/20130220125545_remove_merge_status_from_merge_request.rb10
-rw-r--r--db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb6
-rw-r--r--db/migrate/20130304104623_add_state_to_user.rb6
-rw-r--r--db/migrate/20130304104740_convert_blocked_to_state.rb15
-rw-r--r--db/migrate/20130304105317_remove_blocked_from_user.rb10
-rw-r--r--db/migrate/20130315124931_user_color_scheme.rb15
-rw-r--r--db/migrate/20130318212250_add_snippets_to_features.rb6
-rw-r--r--db/migrate/20130319214458_create_forked_project_links.rb14
-rw-r--r--db/migrate/20130323174317_add_private_to_snippets.rb6
-rw-r--r--db/migrate/20130324151736_add_type_to_snippets.rb6
-rw-r--r--db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb10
-rw-r--r--db/migrate/20130324203535_add_type_value_for_snippets.rb9
-rw-r--r--db/migrate/20130325173941_add_notification_level_to_user.rb6
-rw-r--r--db/migrate/20130326142630_add_index_to_users_authentication_token.rb6
-rw-r--r--db/migrate/20130403003950_add_last_activity_column_into_project.rb24
-rw-r--r--db/migrate/20130404164628_add_notification_level_to_user_project.rb6
-rw-r--r--db/migrate/20130410175022_remove_wiki_table.rb10
-rw-r--r--db/migrate/20130419190306_allow_merges_for_forks.rb20
-rw-r--r--db/migrate/20130506085413_add_type_to_key.rb6
-rw-r--r--db/migrate/20130506090604_create_deploy_keys_projects.rb13
-rw-r--r--db/migrate/20130506095501_remove_project_id_from_key.rb23
-rw-r--r--db/migrate/20130522141856_add_more_fields_to_service.rb7
-rw-r--r--db/migrate/20130528184641_add_system_to_notes.rb17
-rw-r--r--db/migrate/20130611210815_increase_snippet_text_column_size.rb10
-rw-r--r--db/migrate/20130613165816_add_password_expires_at_to_users.rb6
-rw-r--r--db/migrate/20130613173246_add_created_by_id_to_user.rb6
-rw-r--r--db/migrate/20130614132337_add_improted_to_project.rb6
-rw-r--r--db/migrate/20130617095603_create_users_groups.rb14
-rw-r--r--db/migrate/20130621195223_add_notification_level_to_user_group.rb6
-rw-r--r--db/migrate/20130622115340_add_more_db_index.rb13
-rw-r--r--db/migrate/20130624162710_add_fingerprint_to_key.rb7
-rw-r--r--db/migrate/20130711063759_create_project_group_links.rb13
-rw-r--r--db/migrate/20130804151314_add_st_diff_to_note.rb6
-rw-r--r--db/migrate/20130809124851_add_permission_check_to_user.rb6
-rw-r--r--db/migrate/20130812143708_add_import_url_to_project.rb6
-rw-r--r--db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb7
-rw-r--r--db/migrate/20130820102832_add_access_to_project_group_link.rb6
-rw-r--r--db/migrate/20130821090530_remove_deprecated_tables.rb12
-rw-r--r--db/migrate/20130821090531_add_internal_ids_to_milestones.rb6
-rw-r--r--db/migrate/20130909132950_add_description_to_merge_request.rb6
-rw-r--r--db/migrate/20130926081215_change_owner_id_for_group.rb10
-rw-r--r--db/migrate/20131005191208_add_avatar_to_users.rb6
-rw-r--r--db/migrate/20131009115346_add_confirmable_to_users.rb16
-rw-r--r--db/migrate/20131106151520_remove_default_branch.rb10
-rw-r--r--db/migrate/20131112114325_create_broadcast_messages.rb15
-rw-r--r--db/migrate/20131112220935_add_visibility_level_to_projects.rb16
-rw-r--r--db/migrate/20131129154016_add_archived_to_projects.rb6
-rw-r--r--db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb7
-rw-r--r--db/migrate/20131202192556_add_event_fields_for_web_hook.rb8
-rw-r--r--db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb6
-rw-r--r--db/migrate/20131217102743_add_recipients_to_service.rb6
-rw-r--r--db/migrate/20140116231608_add_website_url_to_users.rb6
-rw-r--r--db/migrate/20140122112253_create_merge_request_diffs.rb24
-rw-r--r--db/migrate/20140122114406_migrate_mr_diffs.rb10
-rw-r--r--db/migrate/20140122122549_remove_m_rdiff_fields.rb22
-rw-r--r--db/migrate/20140125162722_add_avatar_to_projects.rb6
-rw-r--r--db/migrate/20140127170938_add_group_avatars.rb6
-rw-r--r--db/migrate/20140209025651_create_emails.rb16
-rw-r--r--db/migrate/20140214102325_add_api_key_to_services.rb6
-rw-r--r--db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb6
-rw-r--r--db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb6
-rw-r--r--db/migrate/20140312145357_add_import_status_to_project.rb6
-rw-r--r--db/migrate/20140313092127_init_schema.rb337
-rw-r--r--db/migrate/20140313092127_migrate_already_imported_projects.rb15
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb12
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb22
-rw-r--r--db/migrate/20170301101006_add_ci_runner_namespaces.rb17
-rw-r--r--db/migrate/20170906133745_add_runners_token_to_groups.rb9
-rw-r--r--db/migrate/20180326202229_create_ci_build_trace_chunks.rb17
-rw-r--r--db/migrate/20180403035759_create_project_ci_cd_settings.rb68
-rw-r--r--db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb13
-rw-r--r--db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb66
-rw-r--r--db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb15
-rw-r--r--db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb40
-rw-r--r--db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb16
-rw-r--r--db/migrate/20180417101940_add_index_to_ci_stage.rb9
-rw-r--r--db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb15
-rw-r--r--db/migrate/20180420010016_add_pipeline_build_foreign_key.rb27
-rw-r--r--db/migrate/20180420010616_cleanup_build_stage_migration.rb61
-rw-r--r--db/migrate/20180424090541_add_enforce_terms_to_application_settings.rb9
-rw-r--r--db/migrate/20180424134533_create_application_setting_terms.rb13
-rw-r--r--db/migrate/20180425075446_create_term_agreements.rb28
-rw-r--r--db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb27
-rw-r--r--db/migrate/20180426102016_add_accepted_term_to_users.rb23
-rw-r--r--db/migrate/20180430101916_add_runner_type_to_ci_runners.rb9
-rw-r--r--db/migrate/20180502122856_create_project_mirror_data.rb20
-rw-r--r--db/migrate/20180503131624_create_remote_mirrors.rb33
-rw-r--r--db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb15
-rw-r--r--db/migrate/20180503150427_add_index_to_namespaces_runners_token.rb20
-rw-r--r--db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb15
-rw-r--r--db/migrate/20180503175054_add_indexes_to_project_mirror_data.rb17
-rw-r--r--db/migrate/20180503193542_add_indexes_to_remote_mirror.rb15
-rw-r--r--db/migrate/20180503193953_add_mirror_available_to_application_settings.rb15
-rw-r--r--db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb11
-rw-r--r--db/migrate/20180504195842_project_name_lower_index.rb32
-rw-r--r--db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb11
-rw-r--r--db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb21
-rw-r--r--db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb31
-rw-r--r--db/migrate/20180508135515_set_runner_type_not_null.rb9
-rw-r--r--db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb15
-rw-r--r--db/migrate/20180515121227_create_notes_diff_files.rb21
-rw-r--r--db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb38
-rw-r--r--db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb2
-rw-r--r--db/migrate/gpg_keys_limits_to_mysql.rb15
-rw-r--r--db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb9
-rw-r--r--db/optional_migrations/composite_primary_keys.rb63
-rw-r--r--db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb34
-rw-r--r--db/post_migrate/20180420080616_schedule_stages_index_migration.rb29
-rw-r--r--db/post_migrate/20180430143705_backfill_runner_type_for_ci_runners_post_migrate.rb23
-rw-r--r--db/post_migrate/20180502134117_migrate_import_attributes_data_from_projects_to_project_mirror_data.rb38
-rw-r--r--db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb43
-rw-r--r--db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb21
-rw-r--r--db/post_migrate/20180514161336_remove_gemnasium_service.rb15
-rw-r--r--db/schema.rb131
-rw-r--r--doc/README.md42
-rw-r--r--doc/administration/auth/jwt.md2
-rw-r--r--doc/administration/external_database.md17
-rw-r--r--doc/administration/high_availability/nfs.md96
-rw-r--r--doc/administration/high_availability/redis.md43
-rw-r--r--doc/administration/index.md5
-rw-r--r--doc/administration/job_artifacts.md4
-rw-r--r--doc/administration/job_traces.md95
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md6
-rw-r--r--doc/administration/monitoring/prometheus/index.md2
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md2
-rw-r--r--doc/administration/pages/index.md44
-rw-r--r--doc/administration/raketasks/project_import_export.md2
-rw-r--r--doc/administration/repository_checks.md26
-rw-r--r--doc/administration/uploads.md2
-rw-r--r--doc/api/README.md27
-rw-r--r--doc/api/discussions.md585
-rw-r--r--doc/api/group_badges.md2
-rw-r--r--doc/api/group_milestones.md2
-rw-r--r--doc/api/groups.md7
-rw-r--r--doc/api/issues.md13
-rw-r--r--doc/api/jobs.md4
-rw-r--r--doc/api/markdown.md29
-rw-r--r--doc/api/merge_requests.md13
-rw-r--r--doc/api/notes.md9
-rw-r--r--doc/api/pipeline_schedules.md4
-rw-r--r--doc/api/pipelines.md1
-rw-r--r--doc/api/project_import_export.md8
-rw-r--r--doc/api/projects.md34
-rw-r--r--doc/api/runners.md83
-rw-r--r--doc/api/services.md13
-rw-r--r--doc/api/settings.md6
-rw-r--r--doc/api/users.md1
-rw-r--r--doc/articles/openshift_and_gitlab/index.md5
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/environments.md3
-rw-r--r--doc/ci/examples/code_climate.md25
-rw-r--r--doc/ci/examples/container_scanning.md26
-rw-r--r--doc/ci/examples/dast.md6
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md2
-rw-r--r--doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md14
-rw-r--r--doc/ci/pipelines.md5
-rw-r--r--doc/ci/quick_start/README.md4
-rw-r--r--doc/ci/runners/README.md39
-rw-r--r--doc/ci/services/docker-services.md8
-rw-r--r--doc/ci/variables/README.md23
-rw-r--r--doc/ci/yaml/README.md33
-rw-r--r--doc/container_registry/README.md2
-rw-r--r--doc/customization/issue_closing.md7
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/background_migrations.md4
-rw-r--r--doc/development/changelog.md2
-rw-r--r--doc/development/code_review.md5
-rw-r--r--doc/development/database_debugging.md2
-rw-r--r--doc/development/diffs.md115
-rw-r--r--doc/development/doc_styleguide.md122
-rw-r--r--doc/development/ee_features.md40
-rw-r--r--doc/development/fe_guide/development_process.md77
-rw-r--r--doc/development/fe_guide/icons.md114
-rw-r--r--doc/development/fe_guide/index.md58
-rw-r--r--doc/development/fe_guide/style_guide_js.md12
-rw-r--r--doc/development/fe_guide/vue.md434
-rw-r--r--doc/development/fe_guide/vuex.md374
-rw-r--r--doc/development/file_storage.md2
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/development/img/state-model-issue.pngbin7713 -> 0 bytes
-rw-r--r--doc/development/img/state-model-legend.pngbin8496 -> 0 bytes
-rw-r--r--doc/development/img/state-model-merge-request.pngbin12459 -> 0 bytes
-rw-r--r--doc/development/merge_request_performance_guidelines.md2
-rw-r--r--doc/development/new_fe_guide/development/accessibility.md49
-rw-r--r--doc/development/object_state_models.md25
-rw-r--r--doc/development/ordering_table_columns.md2
-rw-r--r--doc/development/query_recorder.md2
-rw-r--r--doc/development/rake_tasks.md5
-rw-r--r--doc/development/testing_guide/best_practices.md88
-rw-r--r--doc/development/testing_guide/ci.md3
-rw-r--r--doc/development/testing_guide/end_to_end_tests.md6
-rw-r--r--doc/development/testing_guide/frontend_testing.md140
-rw-r--r--doc/development/testing_guide/index.md15
-rw-r--r--doc/development/testing_guide/testing_levels.md3
-rw-r--r--doc/development/testing_guide/testing_rake_tasks.md2
-rw-r--r--doc/development/ux_guide/components.md4
-rw-r--r--doc/development/what_requires_downtime.md2
-rw-r--r--doc/development/writing_documentation.md19
-rw-r--r--doc/downgrade_ee_to_ce/README.md21
-rw-r--r--doc/install/README.md1
-rw-r--r--doc/install/azure/index.md5
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/docker.md2
-rw-r--r--doc/install/google_cloud_platform/index.md6
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/kubernetes/gitlab_chart.md488
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md2
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md14
-rw-r--r--doc/install/kubernetes/index.md13
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/install/requirements.md4
-rw-r--r--doc/integration/github.md4
-rw-r--r--doc/integration/shibboleth.md2
-rw-r--r--doc/legal/README.md3
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md5
-rw-r--r--doc/legal/individual_contributor_license_agreement.md5
-rw-r--r--doc/raketasks/backup_restore.md54
-rw-r--r--doc/raketasks/features.md2
-rw-r--r--doc/raketasks/web_hooks.md2
-rw-r--r--doc/security/img/outbound_requests_section.pngbin0 -> 18064 bytes
-rw-r--r--doc/security/webhooks.md13
-rw-r--r--doc/ssh/README.md4
-rw-r--r--doc/topics/autodevops/img/rollout_enabled.pngbin0 -> 19536 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_disabled.pngbin0 -> 13837 bytes
-rw-r--r--doc/topics/autodevops/img/rollout_staging_enabled.pngbin0 -> 17306 bytes
-rw-r--r--doc/topics/autodevops/img/staging_enabled.pngbin0 -> 17929 bytes
-rw-r--r--doc/topics/autodevops/index.md132
-rw-r--r--doc/topics/autodevops/quick_start_guide.md10
-rw-r--r--doc/topics/git/how_to_install_git/index.md1
-rw-r--r--doc/university/README.md1
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/university/high-availability/aws/README.md4
-rw-r--r--doc/university/support/README.md2
-rw-r--r--doc/university/training/end-user/README.md2
-rw-r--r--doc/university/training/gitlab_flow.md30
-rw-r--r--doc/university/training/topics/gitlab_flow.md56
-rw-r--r--doc/university/training/topics/merge_requests.md2
-rw-r--r--doc/university/training/topics/tags.md2
-rw-r--r--doc/university/training/user_training.md2
-rw-r--r--doc/update/10.7-to-10.8.md362
-rw-r--r--doc/user/admin_area/labels.md2
-rw-r--r--doc/user/admin_area/settings/img/enforce_terms.pngbin0 -> 51979 bytes
-rw-r--r--doc/user/admin_area/settings/img/respond_to_terms.pngbin0 -> 205994 bytes
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md2
-rw-r--r--doc/user/admin_area/settings/terms.md38
-rw-r--r--doc/user/gitlab_com/index.md7
-rw-r--r--doc/user/group/img/groups.pngbin202498 -> 249533 bytes
-rw-r--r--doc/user/group/img/new_group_from_groups.pngbin97271 -> 109302 bytes
-rw-r--r--doc/user/group/img/new_group_from_other_pages.pngbin70899 -> 90423 bytes
-rw-r--r--doc/user/group/index.md20
-rw-r--r--doc/user/group/subgroups/index.md2
-rw-r--r--doc/user/index.md6
-rw-r--r--doc/user/permissions.md4
-rw-r--r--doc/user/profile/active_sessions.md20
-rw-r--r--doc/user/profile/img/active_sessions_list.pngbin0 -> 41649 bytes
-rw-r--r--doc/user/profile/index.md1
-rw-r--r--doc/user/project/bulk_editing.md17
-rw-r--r--doc/user/project/clusters/index.md1
-rw-r--r--doc/user/project/deploy_tokens/index.md12
-rw-r--r--doc/user/project/integrations/jira.md2
-rw-r--r--doc/user/project/integrations/project_services.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md10
-rw-r--r--doc/user/project/issues/closing_issues.md6
-rw-r--r--doc/user/project/issues/crosslinking_issues.md2
-rw-r--r--doc/user/project/issues/index.md4
-rw-r--r--doc/user/project/issues/issues_functionalities.md4
-rw-r--r--doc/user/project/merge_requests/index.md4
-rw-r--r--doc/user/project/merge_requests/maintainer_access.md2
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md2
-rw-r--r--doc/user/project/milestones/index.md2
-rw-r--r--doc/user/project/pages/getting_started_part_three.md2
-rw-r--r--doc/user/project/pages/getting_started_part_two.md6
-rw-r--r--doc/user/project/pages/img/remove_fork_relationship.png (renamed from doc/user/project/pages/img/remove_fork_relashionship.png)bin13642 -> 13642 bytes
-rw-r--r--doc/user/project/pages/index.md40
-rw-r--r--doc/user/project/pages/introduction.md6
-rw-r--r--doc/user/project/quick_actions.md7
-rw-r--r--doc/user/project/repository/reducing_the_repo_size_using_git.md2
-rw-r--r--doc/user/project/settings/import_export.md4
-rw-r--r--doc/user/project/web_ide/img/commit_changes.pngbin672321 -> 244172 bytes
-rw-r--r--doc/user/project/web_ide/index.md23
-rw-r--r--doc/user/search/index.md2
-rw-r--r--doc/user/snippets.md33
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md10
-rw-r--r--doc/workflow/merge_requests.md2
-rw-r--r--doc/workflow/notifications.md4
-rw-r--r--doc/workflow/protected_branches.md2
-rw-r--r--doc/workflow/repository_mirroring.md111
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.pngbin0 -> 9512 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.pngbin0 -> 20739 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.pngbin0 -> 16538 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.pngbin0 -> 16765 bytes
-rw-r--r--doc/workflow/repository_mirroring/repository_mirroring_push_settings.pngbin0 -> 18226 bytes
-rw-r--r--doc/workflow/shortcuts.md22
-rw-r--r--doc/workflow/todos.md7
-rw-r--r--features/project/builds/artifacts.feature65
-rw-r--r--features/project/commits/commits.feature96
-rw-r--r--features/project/commits/diff_comments.feature96
-rw-r--r--features/project/deploy_keys.feature46
-rw-r--r--features/project/ff_merge_requests.feature41
-rw-r--r--features/project/find_file.feature42
-rw-r--r--features/project/forked_merge_requests.feature51
-rw-r--r--features/project/issues/references.feature33
-rw-r--r--features/project/merge_requests/references.feature31
-rw-r--r--features/project/source/markdown_render.feature147
-rw-r--r--features/steps/group/members.rb68
-rw-r--r--features/steps/profile/notifications.rb20
-rw-r--r--features/steps/project/builds/artifacts.rb98
-rw-r--r--features/steps/project/commits/branches.rb32
-rw-r--r--features/steps/project/commits/comments.rb6
-rw-r--r--features/steps/project/commits/commits.rb192
-rw-r--r--features/steps/project/commits/diff_comments.rb6
-rw-r--r--features/steps/project/create.rb23
-rw-r--r--features/steps/project/deploy_keys.rb89
-rw-r--r--features/steps/project/ff_merge_requests.rb87
-rw-r--r--features/steps/project/forked_merge_requests.rb139
-rw-r--r--features/steps/project/issues/filter_labels.rb61
-rw-r--r--features/steps/project/issues/issues.rb175
-rw-r--r--features/steps/project/issues/milestones.rb20
-rw-r--r--features/steps/project/issues/references.rb7
-rw-r--r--features/steps/project/merge_requests/references.rb7
-rw-r--r--features/steps/project/project_find_file.rb72
-rw-r--r--features/steps/project/source/browse_files.rb435
-rw-r--r--features/steps/project/source/markdown_render.rb317
-rw-r--r--features/steps/shared/active_tab.rb32
-rw-r--r--features/steps/shared/admin.rb11
-rw-r--r--features/steps/shared/authentication.rb75
-rw-r--r--features/steps/shared/builds.rb53
-rw-r--r--features/steps/shared/diff_note.rb237
-rw-r--r--features/steps/shared/group.rb50
-rw-r--r--features/steps/shared/issuable.rb167
-rw-r--r--features/steps/shared/markdown.rb20
-rw-r--r--features/steps/shared/note.rb25
-rw-r--r--features/steps/shared/paths.rb438
-rw-r--r--features/steps/shared/project.rb132
-rw-r--r--features/steps/shared/project_tab.rb66
-rw-r--r--features/steps/shared/shortcuts.rb18
-rw-r--r--features/steps/shared/sidebar_active_tab.rb31
-rw-r--r--features/steps/shared/user.rb41
-rw-r--r--features/support/capybara.rb50
-rw-r--r--features/support/db_cleaner.rb11
-rw-r--r--features/support/env.rb56
-rw-r--r--features/support/gitaly.rb3
-rw-r--r--features/support/login_helpers.rb19
-rw-r--r--features/support/rerun.rb16
-rw-r--r--lib/api/api.rb19
-rw-r--r--lib/api/api_guard.rb12
-rw-r--r--lib/api/discussions.rb88
-rw-r--r--lib/api/entities.rb45
-rw-r--r--lib/api/groups.rb21
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/custom_attributes.rb3
-rw-r--r--lib/api/helpers/internal_helpers.rb6
-rw-r--r--lib/api/helpers/notes_helpers.rb60
-rw-r--r--lib/api/helpers/pagination.rb253
-rw-r--r--lib/api/helpers/project_snapshots_helpers.rb25
-rw-r--r--lib/api/helpers/related_resources_helpers.rb9
-rw-r--r--lib/api/internal.rb8
-rw-r--r--lib/api/issues.rb12
-rw-r--r--lib/api/markdown.rb33
-rw-r--r--lib/api/merge_requests.rb9
-rw-r--r--lib/api/milestone_responses.rb2
-rw-r--r--lib/api/notes.rb30
-rw-r--r--lib/api/pipelines.rb1
-rw-r--r--lib/api/project_snapshots.rb19
-rw-r--r--lib/api/projects.rb10
-rw-r--r--lib/api/runner.rb30
-rw-r--r--lib/api/runners.rb23
-rw-r--r--lib/api/settings.rb2
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/api/v3/groups.rb4
-rw-r--r--lib/api/v3/runners.rb2
-rw-r--r--lib/api/v3/settings.rb2
-rw-r--r--lib/api/version.rb2
-rw-r--r--lib/backup/artifacts.rb6
-rw-r--r--lib/backup/builds.rb6
-rw-r--r--lib/backup/database.rb16
-rw-r--r--lib/backup/files.rb2
-rw-r--r--lib/backup/helper.rb14
-rw-r--r--lib/backup/lfs.rb6
-rw-r--r--lib/backup/manager.rb55
-rw-r--r--lib/backup/pages.rb6
-rw-r--r--lib/backup/registry.rb6
-rw-r--r--lib/backup/repository.rb142
-rw-r--r--lib/backup/uploads.rb6
-rw-r--r--lib/banzai/filter/commit_trailers_filter.rb1
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb3
-rw-r--r--lib/banzai/filter/plantuml_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/feature.rb17
-rw-r--r--lib/gitlab.rb33
-rw-r--r--lib/gitlab/auth/blocked_user_tracker.rb4
-rw-r--r--lib/gitlab/auth/ldap/access.rb41
-rw-r--r--lib/gitlab/auth/ldap/config.rb18
-rw-r--r--lib/gitlab/auth/ldap/user.rb9
-rw-r--r--lib/gitlab/auth/o_auth/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/o_auth/user.rb14
-rw-r--r--lib/gitlab/auth/omniauth_identity_linker_base.rb51
-rw-r--r--lib/gitlab/auth/saml/config.rb4
-rw-r--r--lib/gitlab/auth/saml/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/saml/user.rb17
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb33
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb10
-rw-r--r--lib/gitlab/background_migration/migrate_stage_index.rb47
-rw-r--r--lib/gitlab/background_migration/populate_import_state.rb39
-rw-r--r--lib/gitlab/background_migration/rollback_import_state_data.rb40
-rw-r--r--lib/gitlab/bare_repository_import/importer.rb9
-rw-r--r--lib/gitlab/base_doorkeeper_controller.rb8
-rw-r--r--lib/gitlab/build_access.rb12
-rw-r--r--lib/gitlab/ci/cron_parser.rb8
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb3
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb6
-rw-r--r--lib/gitlab/ci/pipeline/expression.rb10
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb29
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb33
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb8
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb9
-rw-r--r--lib/gitlab/ci/pipeline/preloader.rb28
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb1
-rw-r--r--lib/gitlab/ci/trace.rb32
-rw-r--r--lib/gitlab/ci/trace/chunked_io.rb231
-rw-r--r--lib/gitlab/ci/trace/stream.rb11
-rw-r--r--lib/gitlab/current_settings.rb44
-rw-r--r--lib/gitlab/database.rb2
-rw-r--r--lib/gitlab/database/arel_methods.rb18
-rw-r--r--lib/gitlab/database/count.rb86
-rw-r--r--lib/gitlab/database/migration_helpers.rb4
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb10
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb13
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb2
-rw-r--r--lib/gitlab/diff/file.rb7
-rw-r--r--lib/gitlab/diff/file_collection/base.rb2
-rw-r--r--lib/gitlab/diff/position.rb4
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb2
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb3
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb16
-rw-r--r--lib/gitlab/email/reply_parser.rb7
-rw-r--r--lib/gitlab/file_detector.rb1
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/blame.rb10
-rw-r--r--lib/gitlab/git/blob.rb26
-rw-r--r--lib/gitlab/git/commit.rb89
-rw-r--r--lib/gitlab/git/committer_with_hooks.rb47
-rw-r--r--lib/gitlab/git/gitlab_projects.rb5
-rw-r--r--lib/gitlab/git/path_helper.rb2
-rw-r--r--lib/gitlab/git/raw_diff_change.rb17
-rw-r--r--lib/gitlab/git/remote_repository.rb7
-rw-r--r--lib/gitlab/git/repository.rb257
-rw-r--r--lib/gitlab/git/repository_mirroring.rb6
-rw-r--r--lib/gitlab/git/tag.rb88
-rw-r--r--lib/gitlab/git/wiki.rb65
-rw-r--r--lib/gitlab/git_access.rb12
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb16
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb18
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb38
-rw-r--r--lib/gitlab/gitaly_client/storage_service.rb15
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb11
-rw-r--r--lib/gitlab/gitaly_client/util.rb14
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb2
-rw-r--r--lib/gitlab/github_import/parallel_importer.rb3
-rw-r--r--lib/gitlab/gl_id.rb8
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml6
-rw-r--r--lib/gitlab/import_export/relation_factory.rb5
-rw-r--r--lib/gitlab/incoming_email.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb3
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb3
-rw-r--r--lib/gitlab/metrics/prometheus.rb8
-rw-r--r--lib/gitlab/metrics/web_transaction.rb18
-rw-r--r--lib/gitlab/middleware/webpack_proxy.rb26
-rw-r--r--lib/gitlab/multi_collection_paginator.rb2
-rw-r--r--lib/gitlab/pages_client.rb117
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/project_template.rb6
-rw-r--r--lib/gitlab/redis/shared_state.rb2
-rw-r--r--lib/gitlab/repo_path.rb19
-rw-r--r--lib/gitlab/serializer/pagination.rb2
-rw-r--r--lib/gitlab/shell.rb60
-rw-r--r--lib/gitlab/untrusted_regexp.rb45
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/gitlab/user_access.rb8
-rw-r--r--lib/gitlab/view/presenter/base.rb4
-rw-r--r--lib/gitlab/webpack/dev_server_middleware.rb26
-rw-r--r--lib/gitlab/webpack/manifest.rb27
-rw-r--r--lib/gitlab/workhorse.rb21
-rw-r--r--lib/omni_auth/strategies/jwt.rb62
-rw-r--r--lib/tasks/gitlab/backup.rake132
-rw-r--r--lib/tasks/gitlab/check.rake5
-rw-r--r--lib/tasks/gitlab/info.rake2
-rw-r--r--lib/tasks/gitlab/list_repos.rake5
-rw-r--r--lib/tasks/gitlab/pages.rake9
-rw-r--r--lib/tasks/gitlab/setup.rake11
-rw-r--r--lib/tasks/gitlab/test.rake1
-rw-r--r--lib/tasks/migrate/add_limits_mysql.rake4
-rw-r--r--lib/tasks/migrate/composite_primary_keys.rake15
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--lib/tasks/spinach.rake60
-rw-r--r--locale/gitlab.pot562
-rw-r--r--package.json68
-rw-r--r--qa/Dockerfile2
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa.rb8
-rw-r--r--qa/qa/factory/repository/push.rb26
-rw-r--r--qa/qa/factory/resource/branch.rb23
-rw-r--r--qa/qa/factory/resource/deploy_key.rb14
-rw-r--r--qa/qa/factory/resource/merge_request.rb6
-rw-r--r--qa/qa/factory/resource/project.rb7
-rw-r--r--qa/qa/factory/resource/secret_variable.rb3
-rw-r--r--qa/qa/git/location.rb2
-rw-r--r--qa/qa/git/repository.rb7
-rw-r--r--qa/qa/page/README.md4
-rw-r--r--qa/qa/page/base.rb4
-rw-r--r--qa/qa/page/menu/main.rb11
-rw-r--r--qa/qa/page/project/job/show.rb22
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb12
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb29
-rw-r--r--qa/qa/page/project/settings/secret_variables.rb20
-rw-r--r--qa/qa/page/project/show.rb8
-rw-r--r--qa/qa/runtime/key/base.rb36
-rw-r--r--qa/qa/runtime/key/ecdsa.rb12
-rw-r--r--qa/qa/runtime/key/ed25519.rb12
-rw-r--r--qa/qa/runtime/key/rsa.rb11
-rw-r--r--qa/qa/runtime/rsa_key.rb21
-rw-r--r--qa/qa/scenario/test/sanity/selectors.rb2
-rw-r--r--qa/qa/service/shellout.rb4
-rw-r--r--qa/qa/specs/features/merge_request/create_spec.rb2
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb3
-rw-r--r--qa/qa/specs/features/project/deploy_key_clone_spec.rb134
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb6
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb9
-rw-r--r--qa/spec/runtime/key/ecdsa_spec.rb18
-rw-r--r--qa/spec/runtime/key/ed25519_spec.rb9
-rw-r--r--qa/spec/runtime/key/rsa_spec.rb9
-rw-r--r--qa/spec/runtime/rsa_key.rb9
-rw-r--r--rubocop/cop/gitlab/has_many_through_scope.rb45
-rw-r--r--rubocop/rubocop.rb3
-rw-r--r--rubocop/spec_helpers.rb2
-rw-r--r--scripts/create_mysql_user.sh1
-rw-r--r--scripts/create_postgres_user.sh4
-rwxr-xr-xscripts/gitaly-test-build37
-rwxr-xr-xscripts/gitaly-test-spawn26
-rw-r--r--scripts/gitaly_test.rb97
-rwxr-xr-xscripts/no-ee-check7
-rw-r--r--scripts/prepare_build.sh20
-rwxr-xr-xscripts/trigger-build-cloud-native61
-rw-r--r--scripts/utils.sh18
-rw-r--r--spec/controllers/application_controller_spec.rb63
-rw-r--r--spec/controllers/concerns/continue_params_spec.rb45
-rw-r--r--spec/controllers/concerns/internal_redirect_spec.rb66
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb15
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb74
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb5
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb5
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb58
-rw-r--r--spec/controllers/profiles_controller_spec.rb4
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb2
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb33
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb310
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb6
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb27
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb30
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb72
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb53
-rw-r--r--spec/controllers/projects/prometheus/metrics_controller_spec.rb73
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb4
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb24
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb17
-rw-r--r--spec/controllers/sessions_controller_spec.rb2
-rw-r--r--spec/controllers/users/terms_controller_spec.rb81
-rw-r--r--spec/db/production/settings_spec.rb6
-rw-r--r--spec/factories/ci/build_trace_chunks.rb7
-rw-r--r--spec/factories/ci/builds.rb5
-rw-r--r--spec/factories/ci/runners.rb2
-rw-r--r--spec/factories/ci/stages.rb1
-rw-r--r--spec/factories/clusters/clusters.rb4
-rw-r--r--spec/factories/commit_statuses.rb1
-rw-r--r--spec/factories/commits.rb2
-rw-r--r--spec/factories/deploy_tokens.rb8
-rw-r--r--spec/factories/gitaly/tag.rb9
-rw-r--r--spec/factories/gpg_key_subkeys.rb2
-rw-r--r--spec/factories/gpg_keys.rb2
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/groups.rb8
-rw-r--r--spec/factories/import_state.rb38
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/namespaces.rb18
-rw-r--r--spec/factories/notes.rb2
-rw-r--r--spec/factories/project_wikis.rb2
-rw-r--r--spec/factories/projects.rb42
-rw-r--r--spec/factories/remote_mirrors.rb6
-rw-r--r--spec/factories/term_agreements.rb6
-rw-r--r--spec/factories/terms.rb5
-rw-r--r--spec/fast_spec_helper.rb10
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_requests_profiles_spec.rb4
-rw-r--r--spec/features/admin/admin_runners_spec.rb41
-rw-r--r--spec/features/admin/admin_settings_spec.rb23
-rw-r--r--spec/features/admin/admin_users_spec.rb8
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb32
-rw-r--r--spec/features/boards/boards_spec.rb52
-rw-r--r--spec/features/boards/issue_ordering_spec.rb34
-rw-r--r--spec/features/boards/modal_filter_spec.rb22
-rw-r--r--spec/features/boards/new_issue_spec.rb7
-rw-r--r--spec/features/boards/sidebar_spec.rb44
-rw-r--r--spec/features/boards/sub_group_project_spec.rb2
-rw-r--r--spec/features/dashboard/groups_list_spec.rb22
-rw-r--r--spec/features/dashboard/milestone_filter_spec.rb49
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb2
-rw-r--r--spec/features/explore/new_menu_spec.rb6
-rw-r--r--spec/features/groups/members/filter_members_spec.rb54
-rw-r--r--spec/features/groups/members/manage_access_requests_spec.rb47
-rw-r--r--spec/features/groups/members/master_manages_access_requests_spec.rb8
-rw-r--r--spec/features/groups/members/search_members_spec.rb2
-rw-r--r--spec/features/groups/settings/group_badges_spec.rb16
-rw-r--r--spec/features/groups/user_browse_projects_group_page_spec.rb2
-rw-r--r--spec/features/invites_spec.rb112
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb140
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb187
-rw-r--r--spec/features/issuables/markdown_references_spec.rb193
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb9
-rw-r--r--spec/features/issues/todo_spec.rb4
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb4
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb6
-rw-r--r--spec/features/labels_hierarchy_spec.rb38
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb8
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb11
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb24
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb66
-rw-r--r--spec/features/milestone_spec.rb2
-rw-r--r--spec/features/oauth_login_spec.rb49
-rw-r--r--spec/features/profiles/active_sessions_spec.rb89
-rw-r--r--spec/features/projects/actve_tabs_spec.rb4
-rw-r--r--spec/features/projects/artifacts/browse_spec.rb67
-rw-r--r--spec/features/projects/artifacts/download_spec.rb61
-rw-r--r--spec/features/projects/artifacts/user_browses_artifacts_spec.rb110
-rw-r--r--spec/features/projects/artifacts/user_downloads_artifacts_spec.rb44
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb18
-rw-r--r--spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb52
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb41
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb170
-rw-r--r--spec/features/projects/commit/comments/user_deletes_comments_spec.rb37
-rw-r--r--spec/features/projects/commit/comments/user_edits_comments_spec.rb42
-rw-r--r--spec/features/projects/commit/user_comments_on_commit_spec.rb110
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb194
-rw-r--r--spec/features/projects/compare_spec.rb69
-rw-r--r--spec/features/projects/deploy_keys_spec.rb6
-rw-r--r--spec/features/projects/environments/environments_spec.rb16
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb240
-rw-r--r--spec/features/projects/files/user_find_file_spec.rb66
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin341299 -> 343091 bytes
-rw-r--r--spec/features/projects/issues/user_sorts_issues_spec.rb2
-rw-r--r--spec/features/projects/issues/user_toggles_subscription_spec.rb8
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb50
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb45
-rw-r--r--spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb9
-rw-r--r--spec/features/projects/merge_requests/user_creates_merge_request_spec.rb80
-rw-r--r--spec/features/projects/merge_requests/user_merges_merge_request_spec.rb43
-rw-r--r--spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb34
-rw-r--r--spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb4
-rw-r--r--spec/features/projects/new_project_spec.rb4
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb53
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb34
-rw-r--r--spec/features/projects/remote_mirror_spec.rb34
-rw-r--r--spec/features/projects/settings/lfs_settings_spec.rb18
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb53
-rw-r--r--spec/features/projects/settings/project_badges_spec.rb16
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb25
-rw-r--r--spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb125
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb8
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb7
-rw-r--r--spec/features/projects/tree/create_file_spec.rb5
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb13
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb2
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb41
-rw-r--r--spec/features/projects/user_views_empty_project_spec.rb43
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb25
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb288
-rw-r--r--spec/features/projects/wiki/user_deletes_wiki_page_spec.rb5
-rw-r--r--spec/features/projects/wiki/user_git_access_wiki_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb8
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb1
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb2
-rw-r--r--spec/features/protected_branches_spec.rb2
-rw-r--r--spec/features/raven_js_spec.rb2
-rw-r--r--spec/features/runners_spec.rb194
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/signed_commits_spec.rb61
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/users/active_sessions_spec.rb69
-rw-r--r--spec/features/users/login_spec.rb143
-rw-r--r--spec/features/users/signup_spec.rb25
-rw-r--r--spec/features/users/terms_spec.rb102
-rw-r--r--spec/features/users/user_browses_projects_on_user_page_spec.rb8
-rw-r--r--spec/finders/group_descendants_finder_spec.rb9
-rw-r--r--spec/finders/groups_finder_spec.rb84
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/finders/personal_projects_finder_spec.rb12
-rw-r--r--spec/finders/pipelines_finder_spec.rb20
-rw-r--r--spec/fixtures/api/schemas/ci_detailed_status.json24
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json2
-rw-r--r--spec/fixtures/api/schemas/job.json24
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/fixtures/api/schemas/pipeline_stage.json24
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/notes.json5
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/projects.json1
-rw-r--r--spec/fixtures/emails/valid_new_issue_with_quote.eml25
-rw-r--r--spec/fixtures/exported-project.gzbin2306 -> 2560 bytes
-rw-r--r--spec/helpers/application_helper_spec.rb151
-rw-r--r--spec/helpers/auth_helper_spec.rb24
-rw-r--r--spec/helpers/avatars_helper_spec.rb139
-rw-r--r--spec/helpers/blob_helper_spec.rb31
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb15
-rw-r--r--spec/helpers/issuables_helper_spec.rb16
-rw-r--r--spec/helpers/milestones_helper_spec.rb54
-rw-r--r--spec/helpers/projects_helper_spec.rb6
-rw-r--r--spec/helpers/users_helper_spec.rb37
-rw-r--r--spec/initializers/6_validations_spec.rb20
-rw-r--r--spec/javascripts/.eslintrc1
-rw-r--r--spec/javascripts/activities_spec.js75
-rw-r--r--spec/javascripts/api_spec.js21
-rw-r--r--spec/javascripts/badges/components/badge_list_spec.js2
-rw-r--r--spec/javascripts/badges/components/badge_settings_spec.js4
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js2
-rw-r--r--spec/javascripts/behaviors/secret_values_spec.js2
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js6
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js2
-rw-r--r--spec/javascripts/blob/sketch/index_spec.js2
-rw-r--r--spec/javascripts/boards/board_list_spec.js4
-rw-r--r--spec/javascripts/boards/issue_card_spec.js40
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js10
-rw-r--r--spec/javascripts/comment_type_toggle_spec.js5
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/commits_spec.js12
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js64
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js137
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js102
-rw-r--r--spec/javascripts/deploy_keys/components/keys_panel_spec.js42
-rw-r--r--spec/javascripts/droplab/hook_spec.js5
-rw-r--r--spec/javascripts/environments/environments_app_spec.js212
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js81
-rw-r--r--spec/javascripts/environments/mock_data.js1
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js17
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js6
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb4
-rw-r--r--spec/javascripts/fixtures/event_filter.html.haml2
-rw-r--r--spec/javascripts/fixtures/issue_sidebar_label.html.haml2
-rw-r--r--spec/javascripts/fixtures/linked_tabs.html.haml10
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml1
-rw-r--r--spec/javascripts/gl_dropdown_spec.js19
-rw-r--r--spec/javascripts/gpg_badges_spec.js4
-rw-r--r--spec/javascripts/groups/components/app_spec.js5
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js5
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js2
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js22
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js2
-rw-r--r--spec/javascripts/ide/components/activity_bar_spec.js92
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js14
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js29
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/form_spec.js149
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js51
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js15
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js21
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js46
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/success_message_spec.js35
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js39
-rw-r--r--spec/javascripts/ide/components/file_finder/index_spec.js308
-rw-r--r--spec/javascripts/ide/components/file_finder/item_spec.js140
-rw-r--r--spec/javascripts/ide/components/ide_context_bar_spec.js37
-rw-r--r--spec/javascripts/ide/components/ide_external_links_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_project_tree_spec.js39
-rw-r--r--spec/javascripts/ide/components/ide_repo_tree_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_review_spec.js69
-rw-r--r--spec/javascripts/ide/components/ide_side_bar_spec.js33
-rw-r--r--spec/javascripts/ide/components/ide_spec.js80
-rw-r--r--spec/javascripts/ide/components/ide_status_bar_spec.js63
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js54
-rw-r--r--spec/javascripts/ide/components/ide_tree_spec.js34
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js22
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js14
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js155
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js91
-rw-r--r--spec/javascripts/ide/components/repo_file_spec.js67
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js54
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js34
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js29
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js68
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js6
-rw-r--r--spec/javascripts/ide/mock_data.js95
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js113
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js192
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js152
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js117
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js101
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js10
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/actions_spec.js289
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/getters_spec.js71
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js120
-rw-r--r--spec/javascripts/ide/stores/mutations/branch_spec.js58
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js84
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js72
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js25
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js15
-rw-r--r--spec/javascripts/issue_spec.js1
-rw-r--r--spec/javascripts/job_spec.js3
-rw-r--r--spec/javascripts/jobs/header_spec.js34
-rw-r--r--spec/javascripts/jobs/sidebar_details_block_spec.js61
-rw-r--r--spec/javascripts/lib/utils/csrf_token_spec.js2
-rw-r--r--spec/javascripts/lib/utils/image_utility_spec.js8
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js8
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js13
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js19
-rw-r--r--spec/javascripts/monitoring/graph/track_line_spec.js10
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js2
-rw-r--r--spec/javascripts/monitoring/graph_spec.js7
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js11
-rw-r--r--spec/javascripts/pager_spec.js43
-rw-r--r--spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js5
-rw-r--r--spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js5
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js2
-rw-r--r--spec/javascripts/pipelines/async_button_spec.js62
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js2
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js42
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js32
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js11
-rw-r--r--spec/javascripts/pipelines/mock_data.js103
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js42
-rw-r--r--spec/javascripts/pipelines/stage_spec.js38
-rw-r--r--spec/javascripts/projects_dropdown/components/app_spec.js41
-rw-r--r--spec/javascripts/projects_dropdown/components/search_spec.js1
-rw-r--r--spec/javascripts/right_sidebar_spec.js4
-rw-r--r--spec/javascripts/search_autocomplete_spec.js4
-rw-r--r--spec/javascripts/settings_panels_spec.js4
-rw-r--r--spec/javascripts/shortcuts_dashboard_navigation_spec.js9
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/sidebar/mock_data.js2
-rw-r--r--spec/javascripts/sidebar/participants_spec.js14
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js7
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js15
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js3
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js19
-rw-r--r--spec/javascripts/test_bundle.js26
-rw-r--r--spec/javascripts/todos_spec.js5
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js2
-rw-r--r--spec/javascripts/u2f/register_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js40
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js46
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js22
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js17
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js8
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/callout_spec.js45
-rw-r--r--spec/javascripts/vue_shared/components/ci_icon_spec.js149
-rw-r--r--spec/javascripts/vue_shared/components/clipboard_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js7
-rw-r--r--spec/javascripts/vue_shared/components/expand_button_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js10
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb212
-rw-r--r--spec/lib/api/helpers/related_resources_helpers_spec.rb16
-rw-r--r--spec/lib/backup/files_spec.rb14
-rw-r--r--spec/lib/backup/manager_spec.rb2
-rw-r--r--spec/lib/backup/repository_spec.rb52
-rw-r--r--spec/lib/banzai/filter/commit_trailers_filter_spec.rb40
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb2
-rw-r--r--spec/lib/gitlab/auth/blocked_user_tracker_spec.rb44
-rw-r--r--spec/lib/gitlab/auth/ldap/access_spec.rb13
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb36
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb13
-rw-r--r--spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb62
-rw-r--r--spec/lib/gitlab/auth/saml/identity_linker_spec.rb48
-rw-r--r--spec/lib/gitlab/auth/user_access_denied_reason_spec.rb35
-rw-r--r--spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb35
-rw-r--r--spec/lib/gitlab/background_migration/populate_import_state_spec.rb38
-rw-r--r--spec/lib/gitlab/background_migration/rollback_import_state_data_spec.rb28
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb21
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/build_access_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb80
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb96
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb99
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/token_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/preloader_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/trace/chunked_io_spec.rb383
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb547
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb547
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb101
-rw-r--r--spec/lib/gitlab/data_builder/wiki_page_spec.rb2
-rw-r--r--spec/lib/gitlab/database/count_spec.rb71
-rw-r--r--spec/lib/gitlab/database_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb54
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb15
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb1
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb1
-rw-r--r--spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb1
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb1
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb18
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb20
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb70
-rw-r--r--spec/lib/gitlab/git/committer_with_hooks_spec.rb154
-rw-r--r--spec/lib/gitlab/git/raw_diff_change_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb242
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb52
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb94
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb22
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb29
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/parallel_importer_spec.rb4
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml8
-rw-r--r--spec/lib/gitlab/import_export/project.json20
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml9
-rw-r--r--spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/wiki_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb8
-rw-r--r--spec/lib/gitlab/kubernetes/helm/base_command_spec.rb18
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb20
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb66
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/prometheus_spec.rb18
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb6
-rw-r--r--spec/lib/gitlab/pages_client_spec.rb172
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb18
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb19
-rw-r--r--spec/lib/gitlab/shell_spec.rb52
-rw-r--r--spec/lib/gitlab/untrusted_regexp_spec.rb53
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb1
-rw-r--r--spec/lib/gitlab/user_access_spec.rb12
-rw-r--r--spec/lib/gitlab/view/presenter/base_spec.rb7
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb34
-rw-r--r--spec/lib/gitlab_spec.rb70
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb87
-rw-r--r--spec/mailers/notify_spec.rb74
-rw-r--r--spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb18
-rw-r--r--spec/migrations/add_pipeline_build_foreign_key_spec.rb32
-rw-r--r--spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb59
-rw-r--r--spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb32
-rw-r--r--spec/migrations/cleanup_build_stage_migration_spec.rb53
-rw-r--r--spec/migrations/create_missing_namespace_for_internal_users_spec.rb42
-rw-r--r--spec/migrations/migrate_import_attributes_data_from_projects_to_project_mirror_data_spec.rb56
-rw-r--r--spec/migrations/schedule_stages_index_migration_spec.rb35
-rw-r--r--spec/models/active_session_spec.rb216
-rw-r--r--spec/models/appearance_spec.rb37
-rw-r--r--spec/models/application_setting/term_spec.rb15
-rw-r--r--spec/models/application_setting_spec.rb31
-rw-r--r--spec/models/blob_viewer/readme_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb143
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb396
-rw-r--r--spec/models/ci/job_artifact_spec.rb95
-rw-r--r--spec/models/ci/pipeline_spec.rb55
-rw-r--r--spec/models/ci/runner_spec.rb331
-rw-r--r--spec/models/ci/stage_spec.rb34
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb12
-rw-r--r--spec/models/clusters/applications/runner_spec.rb9
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb9
-rw-r--r--spec/models/commit_spec.rb85
-rw-r--r--spec/models/commit_status_spec.rb6
-rw-r--r--spec/models/concerns/avatarable_spec.rb16
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb153
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb2
-rw-r--r--spec/models/concerns/group_descendant_spec.rb17
-rw-r--r--spec/models/concerns/issuable_spec.rb31
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb22
-rw-r--r--spec/models/concerns/redis_cacheable_spec.rb83
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb8
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb69
-rw-r--r--spec/models/concerns/sortable_spec.rb108
-rw-r--r--spec/models/concerns/uniquify_spec.rb9
-rw-r--r--spec/models/deploy_token_spec.rb19
-rw-r--r--spec/models/deployment_spec.rb9
-rw-r--r--spec/models/diff_note_spec.rb95
-rw-r--r--spec/models/environment_spec.rb4
-rw-r--r--spec/models/generic_commit_status_spec.rb6
-rw-r--r--spec/models/group_spec.rb99
-rw-r--r--spec/models/guest_spec.rb6
-rw-r--r--spec/models/issue_spec.rb63
-rw-r--r--spec/models/lfs_object_spec.rb43
-rw-r--r--spec/models/members/group_member_spec.rb48
-rw-r--r--spec/models/members/project_member_spec.rb12
-rw-r--r--spec/models/merge_request_spec.rb164
-rw-r--r--spec/models/milestone_spec.rb32
-rw-r--r--spec/models/namespace_spec.rb24
-rw-r--r--spec/models/note_diff_file_spec.rb11
-rw-r--r--spec/models/note_spec.rb17
-rw-r--r--spec/models/notification_setting_spec.rb7
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb24
-rw-r--r--spec/models/project_import_state_spec.rb13
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb33
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb2
-rw-r--r--spec/models/project_spec.rb410
-rw-r--r--spec/models/project_statistics_spec.rb80
-rw-r--r--spec/models/project_wiki_spec.rb21
-rw-r--r--spec/models/remote_mirror_spec.rb267
-rw-r--r--spec/models/repository_spec.rb168
-rw-r--r--spec/models/term_agreement_spec.rb8
-rw-r--r--spec/models/user_spec.rb237
-rw-r--r--spec/models/wiki_page_spec.rb4
-rw-r--r--spec/policies/application_setting/term_policy_spec.rb50
-rw-r--r--spec/policies/ci/build_policy_spec.rb13
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb12
-rw-r--r--spec/policies/global_policy_spec.rb92
-rw-r--r--spec/policies/group_policy_spec.rb27
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/policies/user_policy_spec.rb18
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb37
-rw-r--r--spec/presenters/commit_status_presenter_spec.rb15
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb35
-rw-r--r--spec/presenters/project_presenter_spec.rb13
-rw-r--r--spec/requests/api/discussions_spec.rb33
-rw-r--r--spec/requests/api/environments_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb9
-rw-r--r--spec/requests/api/helpers_spec.rb18
-rw-r--r--spec/requests/api/internal_spec.rb12
-rw-r--r--spec/requests/api/issues_spec.rb309
-rw-r--r--spec/requests/api/jobs_spec.rb6
-rw-r--r--spec/requests/api/markdown_spec.rb112
-rw-r--r--spec/requests/api/merge_requests_spec.rb225
-rw-r--r--spec/requests/api/project_import_spec.rb5
-rw-r--r--spec/requests/api/project_snapshots_spec.rb51
-rw-r--r--spec/requests/api/projects_spec.rb8
-rw-r--r--spec/requests/api/runner_spec.rb118
-rw-r--r--spec/requests/api/runners_spec.rb169
-rw-r--r--spec/requests/api/search_spec.rb2
-rw-r--r--spec/requests/api/settings_spec.rb6
-rw-r--r--spec/requests/api/users_spec.rb39
-rw-r--r--spec/requests/api/v3/builds_spec.rb4
-rw-r--r--spec/requests/api/v3/groups_spec.rb8
-rw-r--r--spec/requests/api/v3/projects_spec.rb2
-rw-r--r--spec/requests/api/version_spec.rb2
-rw-r--r--spec/requests/api/wikis_spec.rb34
-rw-r--r--spec/requests/git_http_spec.rb53
-rw-r--r--spec/requests/openid_connect_spec.rb11
-rw-r--r--spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb74
-rw-r--r--spec/serializers/entity_date_helper_spec.rb55
-rw-r--r--spec/serializers/job_entity_spec.rb63
-rw-r--r--spec/serializers/pipeline_entity_spec.rb7
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb23
-rw-r--r--spec/services/application_settings/update_service_spec.rb57
-rw-r--r--spec/services/applications/create_service_spec.rb14
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb42
-rw-r--r--spec/services/ci/register_job_service_spec.rb99
-rw-r--r--spec/services/ci/retry_build_service_spec.rb6
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb31
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb62
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb33
-rw-r--r--spec/services/clusters/create_service_spec.rb68
-rw-r--r--spec/services/git_push_service_spec.rb66
-rw-r--r--spec/services/groups/destroy_service_spec.rb16
-rw-r--r--spec/services/groups/nested_create_service_spec.rb7
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb28
-rw-r--r--spec/services/keys/destroy_service_spec.rb13
-rw-r--r--spec/services/labels/transfer_service_spec.rb10
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb26
-rw-r--r--spec/services/notes/create_service_spec.rb82
-rw-r--r--spec/services/notes/resolve_service_spec.rb23
-rw-r--r--spec/services/notification_service_spec.rb99
-rw-r--r--spec/services/projects/create_from_template_service_spec.rb24
-rw-r--r--spec/services/projects/create_service_spec.rb5
-rw-r--r--spec/services/projects/destroy_service_spec.rb29
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb14
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb51
-rw-r--r--spec/services/projects/transfer_service_spec.rb6
-rw-r--r--spec/services/projects/update_pages_service_spec.rb61
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb359
-rw-r--r--spec/services/projects/update_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb76
-rw-r--r--spec/services/repository_archive_clean_up_service_spec.rb68
-rw-r--r--spec/services/system_note_service_spec.rb8
-rw-r--r--spec/services/test_hooks/project_service_spec.rb1
-rw-r--r--spec/services/todo_service_spec.rb25
-rw-r--r--spec/services/users/destroy_service_spec.rb4
-rw-r--r--spec/services/users/respond_to_terms_service_spec.rb37
-rw-r--r--spec/services/web_hook_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb2
-rw-r--r--spec/spec_helper.rb89
-rw-r--r--spec/support/api/milestones_shared_examples.rb6
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb28
-rw-r--r--spec/support/board_helpers.rb16
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/capybara_helpers.rb47
-rw-r--r--spec/support/chunked_io/chunked_io_helpers.rb11
-rw-r--r--spec/support/commit_trailers_spec_helper.rb2
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb9
-rw-r--r--spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb33
-rw-r--r--spec/support/cycle_analytics_helpers.rb141
-rw-r--r--spec/support/factory_bot.rb3
-rw-r--r--spec/support/fixture_helpers.rb15
-rw-r--r--spec/support/forgery_protection.rb17
-rwxr-xr-xspec/support/generate-seed-repo-rb2
-rw-r--r--spec/support/gitlab-git-test.git/README.md2
-rw-r--r--spec/support/gitlab_verify.rb45
-rw-r--r--spec/support/helpers/api_helpers.rb (renamed from spec/support/api_helpers.rb)0
-rw-r--r--spec/support/helpers/bare_repo_operations.rb (renamed from spec/support/bare_repo_operations.rb)0
-rw-r--r--spec/support/helpers/board_helpers.rb16
-rw-r--r--spec/support/helpers/capybara_helpers.rb43
-rw-r--r--spec/support/helpers/cookie_helper.rb (renamed from spec/support/cookie_helper.rb)0
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb137
-rw-r--r--spec/support/helpers/database_connection_helpers.rb (renamed from spec/support/database_connection_helpers.rb)0
-rw-r--r--spec/support/helpers/devise_helpers.rb (renamed from spec/support/devise_helpers.rb)0
-rw-r--r--spec/support/helpers/drag_to_helper.rb (renamed from spec/support/drag_to_helper.rb)0
-rw-r--r--spec/support/helpers/dropzone_helper.rb (renamed from spec/support/dropzone_helper.rb)0
-rw-r--r--spec/support/helpers/email_helpers.rb (renamed from spec/support/email_helpers.rb)0
-rw-r--r--spec/support/helpers/fake_migration_classes.rb (renamed from spec/support/fake_migration_classes.rb)0
-rw-r--r--spec/support/helpers/fake_u2f_device.rb (renamed from spec/support/fake_u2f_device.rb)0
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb2
-rw-r--r--spec/support/helpers/filter_item_select_helper.rb (renamed from spec/support/filter_item_select_helper.rb)0
-rw-r--r--spec/support/helpers/filter_spec_helper.rb (renamed from spec/support/filter_spec_helper.rb)0
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb (renamed from spec/support/filtered_search_helpers.rb)0
-rw-r--r--spec/support/helpers/fixture_helpers.rb11
-rw-r--r--spec/support/helpers/git_http_helpers.rb (renamed from spec/support/git_http_helpers.rb)0
-rw-r--r--spec/support/helpers/gitlab_verify_helpers.rb25
-rw-r--r--spec/support/helpers/gpg_helpers.rb (renamed from spec/support/gpg_helpers.rb)0
-rw-r--r--spec/support/helpers/import_spec_helper.rb (renamed from spec/support/import_spec_helper.rb)0
-rw-r--r--spec/support/helpers/input_helper.rb (renamed from spec/support/input_helper.rb)0
-rw-r--r--spec/support/helpers/inspect_requests.rb (renamed from spec/support/inspect_requests.rb)0
-rw-r--r--spec/support/helpers/issue_helpers.rb (renamed from spec/support/issue_helpers.rb)0
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb66
-rw-r--r--spec/support/helpers/jira_service_helper.rb (renamed from spec/support/jira_service_helper.rb)0
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb167
-rw-r--r--spec/support/helpers/ldap_helpers.rb53
-rw-r--r--spec/support/helpers/live_debugger.rb (renamed from spec/support/live_debugger.rb)0
-rw-r--r--spec/support/helpers/login_helpers.rb170
-rw-r--r--spec/support/helpers/markdown_feature.rb (renamed from spec/support/markdown_feature.rb)0
-rw-r--r--spec/support/helpers/merge_request_helpers.rb (renamed from spec/support/merge_request_helpers.rb)0
-rw-r--r--spec/support/helpers/migrations_helpers.rb102
-rw-r--r--spec/support/helpers/mobile_helpers.rb17
-rw-r--r--spec/support/helpers/notification_helpers.rb33
-rw-r--r--spec/support/helpers/project_forks_helper.rb (renamed from spec/support/project_forks_helper.rb)0
-rw-r--r--spec/support/helpers/prometheus_helpers.rb (renamed from spec/support/prometheus_helpers.rb)0
-rw-r--r--spec/support/helpers/query_recorder.rb38
-rw-r--r--spec/support/helpers/quick_actions_helpers.rb10
-rw-r--r--spec/support/helpers/rake_helpers.rb23
-rw-r--r--spec/support/helpers/reactive_caching_helpers.rb (renamed from spec/support/reactive_caching_helpers.rb)0
-rw-r--r--spec/support/helpers/redis_without_keys.rb (renamed from spec/support/redis_without_keys.rb)0
-rw-r--r--spec/support/helpers/reference_parser_helpers.rb (renamed from spec/support/reference_parser_helpers.rb)0
-rw-r--r--spec/support/helpers/repo_helpers.rb (renamed from spec/support/repo_helpers.rb)0
-rw-r--r--spec/support/helpers/routes_helpers.rb7
-rw-r--r--spec/support/helpers/search_helpers.rb (renamed from spec/support/search_helpers.rb)0
-rw-r--r--spec/support/helpers/seed_helper.rb110
-rw-r--r--spec/support/helpers/seed_repo.rb (renamed from spec/support/seed_repo.rb)0
-rw-r--r--spec/support/helpers/select2_helper.rb (renamed from spec/support/select2_helper.rb)0
-rw-r--r--spec/support/helpers/selection_helper.rb (renamed from spec/support/selection_helper.rb)0
-rw-r--r--spec/support/helpers/sorting_helper.rb18
-rw-r--r--spec/support/helpers/stub_configuration.rb102
-rw-r--r--spec/support/helpers/stub_env.rb (renamed from spec/support/stub_env.rb)0
-rw-r--r--spec/support/helpers/stub_feature_flags.rb (renamed from spec/support/stub_feature_flags.rb)0
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb (renamed from spec/support/stub_gitlab_calls.rb)0
-rw-r--r--spec/support/helpers/stub_gitlab_data.rb (renamed from spec/support/stub_gitlab_data.rb)0
-rw-r--r--spec/support/helpers/stub_object_storage.rb48
-rw-r--r--spec/support/helpers/terms_helper.rb19
-rw-r--r--spec/support/helpers/test_env.rb371
-rw-r--r--spec/support/helpers/upload_helpers.rb (renamed from spec/support/upload_helpers.rb)0
-rw-r--r--spec/support/helpers/user_activities_helpers.rb (renamed from spec/support/user_activities_helpers.rb)0
-rw-r--r--spec/support/helpers/wait_for_requests.rb (renamed from spec/support/wait_for_requests.rb)0
-rw-r--r--spec/support/helpers/workhorse_helpers.rb (renamed from spec/support/workhorse_helpers.rb)0
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb46
-rw-r--r--spec/support/javascript_fixtures_helpers.rb66
-rw-r--r--spec/support/json_response.rb5
-rw-r--r--spec/support/json_response_helpers.rb9
-rw-r--r--spec/support/kubernetes_helpers.rb104
-rw-r--r--spec/support/ldap_helpers.rb49
-rw-r--r--spec/support/login_helpers.rb162
-rw-r--r--spec/support/malicious_regexp_shared_examples.rb8
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb (renamed from spec/support/background_migrations_matchers.rb)0
-rw-r--r--spec/support/matchers/exceed_query_limit.rb64
-rw-r--r--spec/support/migrations_helpers.rb92
-rw-r--r--spec/support/mobile_helpers.rb17
-rw-r--r--spec/support/notify_shared_examples.rb199
-rwxr-xr-xspec/support/prepare-gitlab-git-test-for-commit2
-rw-r--r--spec/support/query_recorder.rb103
-rw-r--r--spec/support/rake_helpers.rb19
-rw-r--r--spec/support/redis/redis_helpers.rb18
-rw-r--r--spec/support/routing_helpers.rb3
-rw-r--r--spec/support/rspec.rb12
-rw-r--r--spec/support/seed.rb7
-rw-r--r--spec/support/seed_helper.rb118
-rw-r--r--spec/support/services/clusters/create_service_shared.rb57
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb2
-rw-r--r--spec/support/shared_contexts/email_shared_blocks.rb (renamed from spec/lib/gitlab/email/email_shared_blocks.rb)0
-rw-r--r--spec/support/shared_contexts/json_response_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb (renamed from spec/support/services_shared_context.rb)0
-rw-r--r--spec/support/shared_examples/chat_slash_commands_shared_examples.rb (renamed from spec/support/chat_slash_commands_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb741
-rw-r--r--spec/support/shared_examples/common_system_notes_examples.rb27
-rw-r--r--spec/support/shared_examples/email_format_shared_examples.rb (renamed from spec/support/email_format_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/fast_destroy_all.rb38
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb52
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce.rb4
-rw-r--r--spec/support/shared_examples/gitlab_verify.rb19
-rw-r--r--spec/support/shared_examples/group_members_shared_example.rb (renamed from spec/support/group_members_shared_example.rb)0
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb19
-rw-r--r--spec/support/shared_examples/issuable_shared_examples.rb (renamed from spec/support/issuable_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/issuables_list_metadata_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/issue_tracker_service_shared_example.rb (renamed from spec/support/issue_tracker_service_shared_example.rb)0
-rw-r--r--spec/support/shared_examples/ldap_shared_examples.rb (renamed from spec/support/ldap_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/legacy_path_redirect_shared_examples.rb (renamed from spec/support/legacy_path_redirect_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/malicious_regexp_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/mentionable_shared_examples.rb (renamed from spec/support/mentionable_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/milestone_tabs_examples.rb (renamed from spec/support/milestone_tabs_examples.rb)0
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_spec.rb8
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb63
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/notify_shared_examples.rb231
-rw-r--r--spec/support/shared_examples/reference_parser_shared_examples.rb (renamed from spec/support/reference_parser_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/requests/api/diff_discussions.rb57
-rw-r--r--spec/support/shared_examples/requests/api/resolvable_discussions.rb87
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb429
-rw-r--r--spec/support/shared_examples/snippet_visibility.rb (renamed from spec/support/snippet_visibility.rb)0
-rw-r--r--spec/support/shared_examples/snippets_shared_examples.rb (renamed from spec/support/snippets_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/taskable_shared_examples.rb (renamed from spec/support/taskable_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/time_tracking_shared_examples.rb (renamed from spec/support/time_tracking_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/unique_ip_check_shared_examples.rb (renamed from spec/support/unique_ip_check_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/update_invalid_issuable.rb (renamed from spec/support/update_invalid_issuable.rb)0
-rw-r--r--spec/support/shared_examples/updating_mentions_shared_examples.rb (renamed from spec/support/updating_mentions_shared_examples.rb)0
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb429
-rw-r--r--spec/support/stub_configuration.rb99
-rw-r--r--spec/support/stub_object_storage.rb48
-rw-r--r--spec/support/test_env.rb366
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb31
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb12
-rw-r--r--spec/uploaders/object_storage_spec.rb101
-rw-r--r--spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb165
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb7
-rw-r--r--spec/views/help/index.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb9
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb3
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb62
-rw-r--r--spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb62
-rw-r--r--spec/workers/admin_email_worker_spec.rb41
-rw-r--r--spec/workers/create_note_diff_file_worker_spec.rb16
-rw-r--r--spec/workers/gitlab/github_import/advance_stage_worker_spec.rb6
-rw-r--r--spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb20
-rw-r--r--spec/workers/issue_due_scheduler_worker_spec.rb4
-rw-r--r--spec/workers/mail_scheduler/issue_due_worker_spec.rb4
-rw-r--r--spec/workers/mail_scheduler/notification_service_worker_spec.rb44
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb15
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb4
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb79
-rw-r--r--spec/workers/repository_import_worker_spec.rb8
-rw-r--r--spec/workers/repository_remove_remote_worker_spec.rb50
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb84
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb12
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb9
-rw-r--r--vendor/assets/javascripts/peek.performance_bar.js182
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore3
-rw-r--r--vendor/gitignore/Global/Vim.gitignore2
-rw-r--r--vendor/gitignore/Java.gitignore1
-rw-r--r--vendor/gitignore/Objective-C.gitignore2
-rw-r--r--vendor/gitignore/Swift.gitignore2
-rw-r--r--vendor/gitignore/TeX.gitignore6
-rw-r--r--vendor/gitignore/Unity.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore1
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml213
-rw-r--r--vendor/gitlab-ci-yml/Chef.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Maven.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Swift.gitlab-ci.yml16
-rw-r--r--vendor/ingress/values.yaml5
-rw-r--r--vendor/licenses.csv396
-rw-r--r--vendor/project_templates/express.tar.gzbin5614 -> 4866 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25007 -> 25151 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50945 -> 49430 bytes
-rw-r--r--yarn.lock2512
2839 files changed, 55920 insertions, 31285 deletions
diff --git a/.babelrc b/.babelrc
index 8cf07b73420..50d85f58d69 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,6 +1,9 @@
{
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": {
+ "karma": {
+ "plugins": ["rewire"]
+ },
"coverage": {
"plugins": [
[
@@ -14,7 +17,8 @@
{
"process.env.BABEL_ENV": "coverage"
}
- ]
+ ],
+ "rewire"
]
}
}
diff --git a/.flayignore b/.flayignore
index 3d69bb2c985..7faa6c7bb90 100644
--- a/.flayignore
+++ b/.flayignore
@@ -9,3 +9,8 @@ lib/gitlab/gitaly_client/operation_service.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
+lib/gitlab/ci/trace/chunked_io.rb
+lib/gitlab/gitaly_client/ref_service.rb
+lib/gitlab/gitaly_client/commit_service.rb
+lib/gitlab/git/commit.rb
+lib/gitlab/git/tag.rb
diff --git a/.gitignore b/.gitignore
index e9ff0048c1c..c7d1648615d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,7 +68,10 @@ eslint-report.html
/shared/*
/.gitlab_workhorse_secret
/webpack-report/
+/knapsack/
+/rspec_flaky/
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
/plugins/*
+/.gitlab_pages_secret
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index eed1a50cc8f..ef263a3f106 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
@@ -10,6 +10,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git
paths:
- vendor/ruby
- .yarn-cache/
+ - vendor/gitaly-ruby
.push-cache: &push-cache
cache:
@@ -30,7 +31,6 @@ variables:
GIT_SUBMODULE_STRATEGY: "none"
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
- KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
before_script:
@@ -75,7 +75,7 @@ stages:
.use-mysql: &use-mysql
services:
- - mysql:latest
+ - mysql:5.7
- redis:alpine
.rails5-variables: &rails5-variables
@@ -110,7 +110,7 @@ stages:
# Jobs that only need to pull cache
.dedicated-no-docs-pull-cache-job: &dedicated-no-docs-pull-cache-job
<<: *dedicated-runner
- <<: *except-docs-and-qa
+ <<: *except-docs
<<: *pull-cache
dependencies:
- setup-test-env
@@ -122,6 +122,27 @@ stages:
variables:
SETUP_DB: "false"
+.dedicated-no-docs-and-no-qa-pull-cache-job: &dedicated-no-docs-and-no-qa-pull-cache-job
+ <<: *dedicated-no-docs-pull-cache-job
+ <<: *except-docs-and-qa
+
+.single-script-job: &single-script-job
+ image: ruby:2.4-alpine
+ before_script: []
+ stage: build
+ cache: {}
+ dependencies: []
+ variables: &single-script-job-variables
+ GIT_STRATEGY: none
+ before_script:
+ # We need to download the script rather than clone the repo since the
+ # package-and-qa job will not be able to run when the branch gets
+ # deleted (when merging the MR).
+ - export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
+ - apk add --update openssl
+ - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
+ - chmod 755 $SCRIPT_NAME
+
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
script:
@@ -174,46 +195,6 @@ stages:
<<: *rspec-metadata-mysql
<<: *rails5
-.spinach-metadata: &spinach-metadata
- <<: *dedicated-runner
- <<: *except-docs-and-qa
- <<: *pull-cache
- <<: *rails5-variables
- 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
- - export KNAPSACK_GENERATE_REPORT=true
- - export CACHE_CLASSES=true
- - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- - scripts/gitaly-test-spawn
- - knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)'
- artifacts:
- expire_in: 31d
- when: always
- paths:
- - coverage/
- - knapsack/
- - tmp/capybara/
-
-.spinach-metadata-pg: &spinach-metadata-pg
- <<: *spinach-metadata
- <<: *use-pg
-
-.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5
- <<: *spinach-metadata-pg
- <<: *rails5
-
-.spinach-metadata-mysql: &spinach-metadata-mysql
- <<: *spinach-metadata
- <<: *use-mysql
-
-.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5
- <<: *spinach-metadata-mysql
- <<: *rails5
-
.only-canonical-masters: &only-canonical-masters
only:
- master@gitlab-org/gitlab-ce
@@ -222,10 +203,10 @@ stages:
- master@gitlab/gitlab-ee
.gitlab-setup: &gitlab-setup
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
variables:
- CREATE_DB_USER: "true"
+ SETUP_DB: "false"
script:
# Manually clone gitlab-test and only seed this project in
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
@@ -243,33 +224,24 @@ stages:
.review-docs: &review-docs
<<: *dedicated-runner
<<: *except-qa
- image: ruby:2.4-alpine
- before_script:
- - gem install gitlab --no-doc
- # We need to download the script rather than clone the repo since the
- # review-docs-cleanup job will not be able to run when the branch gets
- # deleted (when merging the MR).
- - apk add --update openssl
- - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs
- - chmod 755 trigger-build-docs
- cache: {}
- dependencies: []
+ <<: *single-script-job
variables:
- GIT_STRATEGY: none
+ <<: *single-script-job-variables
+ SCRIPT_NAME: trigger-build-docs
when: manual
only:
- branches
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate:reset
.migration-paths: &migration-paths
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
variables:
- CREATE_DB_USER: "true"
+ SETUP_DB: "false"
script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ce.git v9.3.0
- git checkout -f FETCH_HEAD
@@ -278,7 +250,7 @@ stages:
- cp config/gitlab.yml.example config/gitlab.yml
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- date
- - git checkout $CI_COMMIT_SHA
+ - git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- . scripts/prepare_build.sh
@@ -289,24 +261,14 @@ stages:
# Trigger a package build in omnibus-gitlab repository
#
package-and-qa:
- <<: *dedicated-runner
- image: ruby:2.4-alpine
- before_script: []
- stage: build
- cache: {}
- when: manual
+ <<: *single-script-job
variables:
- GIT_STRATEGY: none
+ <<: *single-script-job-variables
+ SCRIPT_NAME: trigger-build-omnibus
retry: 0
- before_script:
- # We need to download the script rather than clone the repo since the
- # package-and-qa job will not be able to run when the branch gets
- # deleted (when merging the MR).
- - apk add --update openssl
- - wget https://gitlab.com/$CI_PROJECT_PATH/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
- - chmod 755 trigger-build-omnibus
script:
- - ./trigger-build-omnibus
+ - ./$SCRIPT_NAME
+ when: manual
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
@@ -323,7 +285,8 @@ review-docs-deploy:
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- - ./trigger-build-docs deploy
+ - gem install gitlab --no-ri --no-rdoc
+ - ./$SCRIPT_NAME deploy
# Cleanup remote environment of gitlab-docs
review-docs-cleanup:
@@ -333,7 +296,28 @@ review-docs-cleanup:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
script:
- - ./trigger-build-docs cleanup
+ - gem install gitlab --no-ri --no-rdoc
+ - ./SCRIPT_NAME cleanup
+
+##
+# Trigger a docker image build in CNG (Cloud Native GitLab) repository
+#
+cloud-native-image:
+ image: ruby:2.4-alpine
+ before_script: []
+ stage: build
+ allow_failure: true
+ variables:
+ GIT_DEPTH: "1"
+ cache: {}
+ before_script:
+ - gem install gitlab --no-rdoc --no-ri
+ - chmod 755 ./scripts/trigger-build-cloud-native
+ script:
+ - ./scripts/trigger-build-cloud-native
+ only:
+ - tags@gitlab-org/gitlab-ce
+ - tags@gitlab-org/gitlab-ee
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
@@ -346,9 +330,7 @@ retrieve-tests-metadata:
script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
@@ -364,12 +346,11 @@ update-tests-metadata:
- rspec_flaky/
policy: push
script:
- - retry gem install fog-aws mime-types activesupport
+ - retry gem install fog-aws mime-types activesupport --no-ri --no-rdoc
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
- - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
+ - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
@@ -435,134 +416,131 @@ setup-test-env:
paths:
- tmp/tests
- config/secrets.yml
-
-rspec-pg 0 28: *rspec-metadata-pg
-rspec-pg 1 28: *rspec-metadata-pg
-rspec-pg 2 28: *rspec-metadata-pg
-rspec-pg 3 28: *rspec-metadata-pg
-rspec-pg 4 28: *rspec-metadata-pg
-rspec-pg 5 28: *rspec-metadata-pg
-rspec-pg 6 28: *rspec-metadata-pg
-rspec-pg 7 28: *rspec-metadata-pg
-rspec-pg 8 28: *rspec-metadata-pg
-rspec-pg 9 28: *rspec-metadata-pg
-rspec-pg 10 28: *rspec-metadata-pg
-rspec-pg 11 28: *rspec-metadata-pg
-rspec-pg 12 28: *rspec-metadata-pg
-rspec-pg 13 28: *rspec-metadata-pg
-rspec-pg 14 28: *rspec-metadata-pg
-rspec-pg 15 28: *rspec-metadata-pg
-rspec-pg 16 28: *rspec-metadata-pg
-rspec-pg 17 28: *rspec-metadata-pg
-rspec-pg 18 28: *rspec-metadata-pg
-rspec-pg 19 28: *rspec-metadata-pg
-rspec-pg 20 28: *rspec-metadata-pg
-rspec-pg 21 28: *rspec-metadata-pg
-rspec-pg 22 28: *rspec-metadata-pg
-rspec-pg 23 28: *rspec-metadata-pg
-rspec-pg 24 28: *rspec-metadata-pg
-rspec-pg 25 28: *rspec-metadata-pg
-rspec-pg 26 28: *rspec-metadata-pg
-rspec-pg 27 28: *rspec-metadata-pg
-
-rspec-mysql 0 28: *rspec-metadata-mysql
-rspec-mysql 1 28: *rspec-metadata-mysql
-rspec-mysql 2 28: *rspec-metadata-mysql
-rspec-mysql 3 28: *rspec-metadata-mysql
-rspec-mysql 4 28: *rspec-metadata-mysql
-rspec-mysql 5 28: *rspec-metadata-mysql
-rspec-mysql 6 28: *rspec-metadata-mysql
-rspec-mysql 7 28: *rspec-metadata-mysql
-rspec-mysql 8 28: *rspec-metadata-mysql
-rspec-mysql 9 28: *rspec-metadata-mysql
-rspec-mysql 10 28: *rspec-metadata-mysql
-rspec-mysql 11 28: *rspec-metadata-mysql
-rspec-mysql 12 28: *rspec-metadata-mysql
-rspec-mysql 13 28: *rspec-metadata-mysql
-rspec-mysql 14 28: *rspec-metadata-mysql
-rspec-mysql 15 28: *rspec-metadata-mysql
-rspec-mysql 16 28: *rspec-metadata-mysql
-rspec-mysql 17 28: *rspec-metadata-mysql
-rspec-mysql 18 28: *rspec-metadata-mysql
-rspec-mysql 19 28: *rspec-metadata-mysql
-rspec-mysql 20 28: *rspec-metadata-mysql
-rspec-mysql 21 28: *rspec-metadata-mysql
-rspec-mysql 22 28: *rspec-metadata-mysql
-rspec-mysql 23 28: *rspec-metadata-mysql
-rspec-mysql 24 28: *rspec-metadata-mysql
-rspec-mysql 25 28: *rspec-metadata-mysql
-rspec-mysql 26 28: *rspec-metadata-mysql
-rspec-mysql 27 28: *rspec-metadata-mysql
-
-spinach-pg 0 2: *spinach-metadata-pg
-spinach-pg 1 2: *spinach-metadata-pg
-
-spinach-mysql 0 2: *spinach-metadata-mysql
-spinach-mysql 1 2: *spinach-metadata-mysql
-
-rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5
-rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5
-
-rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5
-rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5
-
-spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5
-spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5
-
-spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5
-spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5
+ - vendor/gitaly-ruby
+
+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
static-analysis:
<<: *dedicated-no-docs-no-db-pull-cache-job
@@ -648,7 +626,7 @@ migration:path-mysql:
<<: *use-mysql
.db-rollback: &db-rollback
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate
@@ -671,7 +649,7 @@ gitlab:setup-mysql:
# Frontend-related jobs
gitlab:assets:compile:
- <<: *dedicated-no-docs-no-db-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
dependencies: []
variables:
NODE_ENV: "production"
@@ -692,7 +670,7 @@ gitlab:assets:compile:
- webpack-report/
karma:
- <<: *dedicated-no-docs-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
@@ -816,7 +794,7 @@ coverage:
- coverage/assets/
lint:javascript:report:
- <<: *dedicated-no-docs-no-db-pull-cache-job
+ <<: *dedicated-no-docs-and-no-qa-pull-cache-job
stage: post-test
dependencies:
- compile-assets
@@ -879,3 +857,15 @@ gitlab_git_test:
cache: {}
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
+
+no_ee_check:
+ <<: *dedicated-runner
+ <<: *except-docs-and-qa
+ variables:
+ SETUP_DB: "false"
+ before_script: []
+ cache: {}
+ script:
+ - scripts/no-ee-check
+ only:
+ - //@gitlab-org/gitlab-ce
diff --git a/.gitlab/issue_templates/Security Developer Workflow.md b/.gitlab/issue_templates/Security Developer Workflow.md
index 8dd447ed121..0c878dbf64c 100644
--- a/.gitlab/issue_templates/Security Developer Workflow.md
+++ b/.gitlab/issue_templates/Security Developer Workflow.md
@@ -28,11 +28,11 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
-[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script
+[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
#### Documentation and final details
-- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links)
+- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md
index 2bb1f374e98..1c4f30d9320 100644
--- a/.gitlab/merge_request_templates/Database Changes.md
+++ b/.gitlab/merge_request_templates/Database Changes.md
@@ -37,12 +37,14 @@ When removing columns, tables, indexes or other structures:
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
- [ ] API support added
- [ ] Tests added for this feature/bug
-- Review
- - [ ] Has been reviewed by Backend
- - [ ] Has been reviewed by Database
+- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
+ - [ ] Has been reviewed by a Backend maintainer
+ - [ ] Has been reviewed by a Database specialist
- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
-- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
+- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
+
+/label ~database
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index d443238b9e1..16b0b5c95e2 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- - 'spec/support/email_format_shared_examples.rb'
+ - 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb'
- - 'spec/support/repo_helpers.rb'
- - 'spec/support/seed_repo.rb'
+ - 'spec/support/helpers/repo_helpers.rb'
+ - 'spec/support/helpers/seed_repo.rb'
# Offense count: 112
# Configuration parameters: Blacklist.
@@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- - 'spec/support/chat_slash_commands_shared_examples.rb'
+ - 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102
# Cop supports --auto-correct.
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 180d377d6f8..3df66033fa8 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -46,8 +46,9 @@ linters:
# - properties
# - @include declarations with inner @content
# - nested rule sets.
+ # Disabled to minimize Bootstrap migration footprint
DeclarationOrder:
- enabled: true
+ enabled: false
# `scss-lint:disable` control comments should be preceded by a comment
# explaining why these linters are being disabled for this file.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d56c86523f5..99cf96035d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,468 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.8.1 (2018-05-23)
+
+### Fixed (9 changes)
+
+- Allow CommitStatus class to use presentable methods. !18979
+- Fix corrupted environment pages with unathorized proxy url. !18989
+- Fixes deploy token variables on Ci::Build. !19047
+- Fix project mirror database inconsistencies when upgrading from EE to CE. !19109
+- Render 404 when prometheus adapter is disabled in Prometheus metrics controller. !19110
+- Fix error when deleting an empty list of refs.
+- Fixed U2F login when used with LDAP.
+- Bump prometheus-client-mmap to 0.9.3 to fix nil exception error.
+- Fix system hook not firing for blocked users when LDAP sign-in is used.
+
+
+## 10.8.0 (2018-05-22)
+
+### Security (3 changes, 1 of them is from the community)
+
+- Update faraday_middlewar to 0.12.2. !18397 (Takuya Noguchi)
+- Serve archive requests with the correct file in all cases.
+- Sanitizes user name to avoid XSS attacks.
+
+### Fixed (47 changes, 11 of them are from the community)
+
+- Refactor CSS to eliminate vertical misalignment of login nav. !16275 (Takuya Noguchi)
+- Fix pipeline status in branch/tag tree page. !17995
+- Allow group owner to enable runners from subgroups (#41981). !18009
+- Fix template selector menu visibility when toggling preview mode in file edit view. !18118 (Fabian Schneider)
+- Fix confirmation modal for deleting a protected branch. !18176 (Paul Bonaud @PaulRbR)
+- Triggering custom hooks by Wiki UI edit. !18251
+- Now `rake cache:clear` will also clear pipeline status cache. !18257
+- Fix `joined` information on project members page. !18290 (Fabian Schneider)
+- Fix missing namespace for some internal users. !18357
+- Show shared projects on group page. !18390
+- Restore label underline color. !18407 (George Tsiolis)
+- Fix undefined `html_escape` method during markdown rendering. !18418
+- Fix unassign slash command preview. !18447
+- Correct text and functionality for delete user / delete user and contributions modal. !18463 (Marc Schwede)
+- Fix discussions API setting created_at for notable in a group or notable in a project in a group with owners. !18464
+- Don't include lfs_file_locks data in export bundle. !18495
+- Reset milestone filter when clicking "Any Milestone" in dashboard. !18531
+- Ensure member notifications are sent after the member actual creation/update in the DB. !18538
+- Update links to /ci/lint with ones to project ci/lint. !18539 (Takuya Noguchi)
+- Fix tabs container styles to make RSS button clickable. !18559
+- Raise NoRepository error for non-valid repositories when calculating repository checksum. !18594
+- Don't automatically remove artifacts for pages jobs after pages:deploy has run. !18628
+- Increase new issue metadata form margin. !18630 (George Tsiolis)
+- Add loading icon padding for pipeline environments. !18631 (George Tsiolis)
+- ShaAttribute no longer stops startup if database is missing. !18726
+- Fix close keyboard shortcuts dialog using the keyboard shortcut. !18783 (Lars Greiss)
+- Fixes database inconsistencies between Community and Enterprise Edition on import state. !18811
+- Add database foreign key constraint between pipelines and build. !18822
+- Fix finding wiki pages when they have invalidly-encoded content. !18856
+- Fix outdated Web IDE welcome copy. !18861
+- fixed copy to blipboard button in embed bar of snippets. !18923 (haseebeqx)
+- Disables RBAC on nginx-ingress. !18947
+- Correct skewed Kubernetes popover illustration. !18949
+- Resolve Import/Export ci_cd_settings error updating the project. !46049
+- Fix project creation for user endpoint when jobs_enabled parameter supplied.
+- 46210 Display logo and user dropdown on mobile for terms page and fix styling.
+- Adds illustration for when job log was erased.
+- Ensure web hook 'blocked URL' errors are stored in web hook logs and properly surfaced to the user.
+- Make toggle markdown preview shortcut only toggle selected field.
+- Verifiy if pipeline has commit idetails and render information in MR widget when branch is deleted.
+- Fixed inconsistent protected branch pill baseline.
+- Fix setting Gitlab metrics content types.
+- Display only generic message on merge error to avoid exposing any potentially sensitive or user unfriendly backend messages.
+- Fix label links update on project transfer.
+- Breaks commit not found message in pipelines table.
+- Adjust issue boards list header label text color.
+- Prevent pipeline actions in dropdown to redirct to a new page.
+
+### Changed (35 changes, 15 of them are from the community)
+
+- Improve tooltips in collapsed right sidebar. !17714
+- Partition job_queue_duration_seconds with jobs_running_for_project. !17730
+- For group dashboard, we no longer show groups which the visitor is not a member of (this applies to admins and auditors). !17884 (Roger Rüttimann)
+- Use RFC 3676 mail signature delimiters. !17979 (Enrico Scholz)
+- Add sha filter to pipelines list API. !18125
+- New CI Job live-trace architecture. !18169
+- Make project deploy keys table more clearly structured. !18279
+- Remove green background from unlock button in admin area. !18288
+- Renamed Overview to Project in the contextual navigation at a project level. !18295 (Constance Okoghenun)
+- Load branches on new merge request page asynchronously. !18315
+- Create settings section for autodevops. !18321
+- Add a comma to the time estimate system notes. !18326
+- Enable specifying variables when executing a manual pipeline. !18440
+- Fix size and position for fork icon. !18449 (George Tsiolis)
+- Refactored activity calendar. !18469 (Enrico Scholz)
+- Small improvements to repository checks. !18484
+- Add 2FA filter to users API for admins only. !18503
+- Align project avatar on small viewports. !18513 (George Tsiolis)
+- Show group and project LFS settings in the interface to Owners and Masters. !18562
+- Update environment item action buttons icons. !18632 (George Tsiolis)
+- Update timeline icon for description edit. !18633 (George Tsiolis)
+- Revert discussion counter height. !18656 (George Tsiolis)
+- Improve quick actions summary preview. !18659 (George Tsiolis)
+- Change font for tables inside diff discussions. !18660 (George Tsiolis)
+- Add padding to profile description. !18663 (George Tsiolis)
+- Break issue title for board card title and issuable header text. !18674 (George Tsiolis)
+- Adds push mirrors to GitLab Community Edition. !18715
+- Inform the user when there are no project import options available. !18716 (George Tsiolis)
+- Improve commit message body rendering and fix responsive compare panels. !18725 (Constance Okoghenun)
+- Reconcile project templates with Auto DevOps. !18737
+- Remove branch name from the status bar of WebIDE.
+- Clean up WebIDE status bar and add useful info.
+- Improve interaction on WebIDE commit panel.
+- Keep current labels visible when editing them in the sidebar.
+- Use VueJS for rendering pipeline stages.
+
+### Performance (26 changes, 11 of them are from the community)
+
+- Move WorkInProgress vue component. !17536 (George Tsiolis)
+- Move ReadyToMerge vue component. !17545 (George Tsiolis)
+- Move BoardBlankState vue component. !17666 (George Tsiolis)
+- Improve DB performance of calculating total artifacts size. !17839
+- Add i18n and update specs for UnresolvedDiscussions vue component. !17866 (George Tsiolis)
+- Introduce new ProjectCiCdSetting model with group_runners_enabled. !18144
+- Move PipelineFailed vue component. !18277 (George Tsiolis)
+- Move TimeTrackingEstimateOnlyPane vue component. !18318 (George Tsiolis)
+- Move TimeTrackingHelpState vue component. !18319 (George Tsiolis)
+- Reduce queries on merge requests list page for merge requests from forks. !18561
+- Destroy build_chunks efficiently with FastDestroyAll module. !18575
+- Improve performance of a service responsible for creating a pipeline. !18582
+- Replace time_ago_in_words with JS-based one. !18607 (Takuya Noguchi)
+- Move TimeTrackingNoTrackingPane vue component. !18676 (George Tsiolis)
+- Move SidebarTimeTracking vue component. !18677 (George Tsiolis)
+- Move TimeTrackingSpentOnlyPane vue component. !18710 (George Tsiolis)
+- Detecting tags containing a commit uses Gitaly by default.
+- Increase cluster applications installer availability using alpine linux mirrors.
+- Compute notification recipients in background jobs.
+- Use persisted diff data instead fetching Git on discussions.
+- Detecting branchnames containing a commit uses Gitaly by default.
+- Detect repository license on Gitaly by default.
+- Finish NamespaceService migration to Gitaly.
+- Check if a ref exists is done by Gitaly by default.
+- Compute Gitlab::Git::Repository#checksum on Gitaly by default.
+- Repository#exists? is always executed through Gitaly.
+
+### Added (22 changes, 10 of them are from the community)
+
+- Allow group masters to configure runners for groups. !9646 (Alexis Reigel)
+- Adds Embedded Snippets Support. !15695 (haseebeqx)
+- Add Copy metadata quick action. !16473 (Mateusz Bajorski)
+- Show Runner's description on job's page. !17321
+- Add deprecation message to dynamic milestone pages. !17505
+- Show new branch/mr button even when branch exists. !17712 (Jacopo Beschi @jacopo-beschi)
+- API: add languages of project GET /projects/:id/languages. !17770 (Roger Rüttimann)
+- Display active sessions and allow the user to revoke any of it. !17867 (Alexis Reigel)
+- Add cron job to email users on issue due date. !17985 (Stuart Nelson)
+- Rubocop rule to avoid returning from a block. !18000 (Jacopo Beschi @jacopo-beschi)
+- Add the signature verfication badge to the compare view. !18245 (Marc Shaw)
+- Expose Deploy Token data as environment varialbes on CI/CD jobs. !18414
+- Show group id in group settings. !18482 (George Tsiolis)
+- Allow admins to enforce accepting Terms of Service on an instance. !18570
+- Add CI_COMMIT_MESSAGE, CI_COMMIT_TITLE and CI_COMMIT_DESCRIPTION predefined variables. !18672
+- Add GCP signup offer to cluster index / create pages. !18684
+- Output some useful information when running the rails console. !18697
+- Display merge commit SHA in merge widget after merge. !18722
+- git SHA is now displayed alongside the GitLab version on the Admin Dashboard.
+- Expose the target commit ID through the tag API.
+- Added fuzzy file finder to web IDE.
+- Add discussion API for merge requests and commits.
+
+### Other (22 changes, 8 of them are from the community)
+
+- Replace the `project/issues/milestones.feature` spinach test with an rspec analog. !18300 (@blackst0ne)
+- Replace the `project/commits/branches.feature` spinach test with an rspec analog. !18302 (@blackst0ne)
+- Replacing gollum libraries for gitlab custom libs. !18343
+- Replace the `project/commits/comments.feature` spinach test with an rspec analog. !18356 (@blackst0ne)
+- Replace "Click" with "Select" to be more inclusive of people with accessibility requirements. !18386 (Mark Lapierre)
+- Remove ahead/behind graphs on project branches on mobile. !18415 (Takuya Noguchi)
+- Replace the `project/source/markdown_render.feature` spinach test with an rspec analog. !18525 (@blackst0ne)
+- Add missing changelog type to docs. !18526 (@blackst0ne)
+- Added Webhook SSRF prevention to documentation. !18532
+- Upgrade underscore.js to 1.9.0. !18578
+- Add documentation about how to use variables to define deploy policies for staging/production environments. !18675
+- Replace the `project/builds/artifacts.feature` spinach test with an rspec analog. !18729 (@blackst0ne)
+- Block access to the API & git for users that did not accept enforced Terms of Service. !18816
+- Transition to atomic internal ids for all models. !44259
+- Removes modal boards store and mixins from global scope.
+- Replace GKE acronym with Google Kubernetes Engine.
+- Replace vue resource with axios for pipelines details page.
+- Enable prometheus monitoring by default.
+- Replace vue resource with axios in pipelines table.
+- Bump lograge to 0.10.0 and remove monkey patch.
+- Improves wording in new pipeline page.
+- Gitaly handles repository forks by default.
+
+
+## 10.7.4 (2018-05-21)
+
+### Fixed (1 change)
+
+- Fix error when deleting an empty list of refs.
+
+
+## 10.7.3 (2018-05-02)
+
+### Fixed (8 changes)
+
+- Fixed wrong avatar URL when the avatar is on object storage. !18092
+- Fix errors on pushing to an empty repository. !18462
+- Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication. !18543
+- Ports omniauth-jwt gem onto GitLab OmniAuth Strategies suite. !18580
+- Fix redirection error for applications using OpenID. !18599
+- Fix commit trailer rendering when Gravatar is disabled.
+- Fix file_store for artifacts and lfs when saving.
+- Fix users not seeing labels from private groups when being a member of a child project.
+
+
+## 10.7.2 (2018-04-25)
+
+### Security (2 changes)
+
+- Serve archive requests with the correct file in all cases.
+- Sanitizes user name to avoid XSS attacks.
+
+
+## 10.7.1 (2018-04-23)
+
+### Fixed (11 changes)
+
+- [API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors` when no value is passed for `order_by` or `sort`. !18393
+- Fix a case with secret variables being empty sometimes. !18400
+- Fix `Trace::HttpIO` can not render multi-byte chars. !18417
+- Fix specifying a non-default ref when requesting an archive using the legacy URL. !18468
+- Respect visibility options and description when importing project from template. !18473
+- Removes 'No Job log' message from build trace. !18523
+- Align action icons in pipeline graph.
+- Fix direct_upload when records with null file_store are used.
+- Removed alert box in IDE when redirecting to new merge request.
+- Fixed IDE not loading for sub groups.
+- Fixed IDE not showing loading state when tree is loading.
+
+### Performance (4 changes)
+
+- Validate project path prior to hitting the database. !18322
+- Add index to file_store on ci_job_artifacts. !18444
+- Fix N+1 queries when loading participants for a commit note.
+- Support Markdown rendering using multiple projects.
+
+### Added (1 change)
+
+- Add an API endpoint to download git repository snapshots. !18173
+
+
+## 10.7.0 (2018-04-22)
+
+### Security (6 changes, 2 of them are from the community)
+
+- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
+- Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0. !17734 (Takuya Noguchi)
+- Update rack-protection to 2.0.1. !17835 (Takuya Noguchi)
+- Adds confidential notes channel for Slack/Mattermost.
+- Fix XSS on diff view stored on filenames.
+- Fix GitLab Auth0 integration signing in the wrong user.
+
+### Fixed (65 changes, 20 of them are from the community)
+
+- File uploads in remote storage now support project renaming. !4597
+- Fixed bug in dropdown selector when selecting the same selection again. !14631 (bitsapien)
+- Fixed group deletion linked to Mattermost. !16209 (Julien Millau)
+- Create commit API and Web IDE obey LFS filters. !16718
+- Set breadcrumb for admin/runners/show. !17431 (Takuya Noguchi)
+- Enable restore rake task to handle nested storage directories. !17516 (Balasankar C)
+- Fix hover style of dropdown items in the right sidebar. !17519
+- Improve empty state for canceled job. !17646
+- Fix generated URL when listing repoitories for import. !17692
+- Use singular in the diff stats if only one line has been changed. !17697 (Jan Beckmann)
+- Long instance urls do not overflow anymore during project creation. !17717
+- Fix importing multiple assignees from GitLab export. !17718
+- Correct copy text for the promote milestone and label modals. !17726
+- Fix search results stripping last endline when parsing the results. !17777 (Jasper Maes)
+- Add read-only banner to all pages. !17798
+- Fix viewing diffs on old merge requests. !17805
+- Fix forking to subgroup via API when namespace is given by name. !17815 (Jan Beckmann)
+- Fix UI breakdown for Create merge request button. !17821 (Takuya Noguchi)
+- Unify format for nested non-task lists. !17823 (Takuya Noguchi)
+- UX re-design branch items with flexbox. !17832 (Takuya Noguchi)
+- Use porcelain commit lookup method on CI::CreatePipelineService. !17911
+- Update dashboard milestones breadcrumb link. !17933 (George Tsiolis)
+- Deleting a MR you are assigned to should decrements counter. !17951 (m b)
+- Update no repository placeholder. !17964 (George Tsiolis)
+- Drop JSON response in Project Milestone along with avoiding error. !17977 (Takuya Noguchi)
+- Fix personal access token clipboard button style. !17978 (Fabian Schneider)
+- Avoid validation errors when running the Pages domain verification service. !17992
+- Project creation will now raise an error if a service template is invalid. !18013
+- Add better LDAP connection handling. !18039
+- Fix autolinking URLs containing ampersands. !18045
+- Fix exceptions raised when migrating pipeline stages in the background. !18076
+- Always display Labels section in issuable sidebar, even when the project has no labels. !18081 (Branka Martinovic)
+- Fixed gitlab:uploads:migrate task ignoring some uploads. !18082
+- Fixed gitlab:uploads:migrate task failing for Groups' avatar. !18088
+- Increase dropdown width in pipeline graph & center action icon. !18089
+- Fix `JobsController#raw` endpoint can not read traces in database. !18101
+- Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`. !18154
+- Adjust 404's for LegacyDiffNote discussion rendering. !18201
+- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
+- Prioritize weight over title when sorting charts. !18233
+- Verify that deploy token has valid access when pulling container registry image. !18260
+- Stop redirecting the page in pipeline main actions.
+- Fixed IDE button opening the wrong URL in tree list.
+- Ensure hooks run when a deploy key without a user pushes.
+- Fix 404 in group boards when moving issue between lists.
+- Display state indicator for issuable references in non-project scope (e.g. when referencing issuables from group scope).
+- Add missing port to artifact links.
+- Fix data race between ObjectStorage background_upload and Pages publishing.
+- Fixes unresolved discussions rendering the error state instead of the diff.
+- Don't show Jump to Discussion button on Issues.
+- Fix bug rendering group icons when forking.
+- Automatically cleanup stale worktrees and lock files upon a push.
+- Use the GitLab version as part of the appearances cache key.
+- Fix Firefox stealing formatting characters on issue notes.
+- Include matching branches and tags in protected branches / tags count. (Jan Beckmann)
+- Fix 500 error when a merge request from a fork has conflicts and has not yet been updated.
+- Test if remote repository exists when importing wikis.
+- Hide emoji popup after multiple spaces. (Jan Beckmann)
+- Fix relative uri when "#" is in branch name. (Jan)
+- Escape Markdown characters properly when using autocomplete.
+- Ignore project internal references in group context.
+- Fix finding wiki file when Gitaly is enabled.
+- Fix listing commit branch/tags that contain special characters.
+- Ensure internal users (ghost, support bot) get assigned a namespace.
+- Fix links to subdirectories of a directory with a plus character in its path.
+
+### Deprecated (1 change)
+
+- Remove support for legacy tar.gz pages artifacts. !18090
+
+### Changed (22 changes, 2 of them are from the community)
+
+- Add yellow favicon when `CANARY=true` to differientate canary environment. !12477
+- Use human readable value build_timeout in Project. !17386
+- Improved visual styles and consistency for commit hash and possible actions across commit lists. !17406
+- Don't create permanent redirect routes. !17521
+- Add empty repo check before running AutoDevOps pipeline. !17605
+- Update wording to specify create/manage project vs group labels in labels dropdown. !17640
+- Add tooltips to icons in lists of issues and merge requests. !17700
+- Change avatar error message to include allowed file formats. !17747 (Fabian Schneider)
+- Polish design for verifying domains. !17767
+- Move email footer info to a single line. !17916
+- Add average and maximum summary statistics to the prometheus dashboard. !17921
+- Add additional cluster usage metrics to usage ping. !17922
+- Move 'Registry' after 'CI/CD' in project navigation sidebar. !18018 (Elias Werberich)
+- Redesign application settings to match project settings. !18019
+- Allow HTTP(s) when git request is made by GitLab CI. !18021
+- Added hover background color to IDE file list rows.
+- Make project avatar in IDE consistent with the rest of GitLab.
+- Show issues of subgroups in group-level issue board.
+- Repository checksum calculation is handled by Gitaly when feature is enabled.
+- Allow viewing timings for AJAX requests in the performance bar.
+- Fixes remove source branch checkbox being visible when user cannot remove the branch.
+- Make /-/ delimiter optional for search endpoints.
+
+### Performance (24 changes, 11 of them are from the community)
+
+- Move AssigneeTitle vue component. !17397 (George Tsiolis)
+- Move TimeTrackingCollapsedState vue component. !17399 (George Tsiolis)
+- Move MemoryGraph and MemoryUsage vue components. !17533 (George Tsiolis)
+- Move UnresolvedDiscussions vue component. !17538 (George Tsiolis)
+- Move NothingToMerge vue component. !17544 (George Tsiolis)
+- Move ShaMismatch vue component. !17546 (George Tsiolis)
+- Stop caching highlighted diffs in Redis unnecessarily. !17746
+- Add i18n and update specs for ShaMismatch vue component. !17870 (George Tsiolis)
+- Update spec import path for vue mount component helper. !17880 (George Tsiolis)
+- Move TimeTrackingComparisonPane vue component. !17931 (George Tsiolis)
+- Improves the performance of projects list page. !17934
+- Remove N+1 query for Noteable association. !17956
+- Improve performance of loading issues with lots of references to merge requests. !17986
+- Reuse root_ref_hash for performance on Branches. !17998 (Takuya Noguchi)
+- Update asciidoctor-plantuml to 0.0.8. !18022 (Takuya Noguchi)
+- Cache personal projects count. !18197
+- Reduce complexity of issuable finder query. !18219
+- Reduce number of queries when viewing a merge request.
+- Free open file descriptors and libgit2 buffers in UpdatePagesService.
+- Memoize Git::Repository#has_visible_content?.
+- Require at least one filter when listing issues or merge requests on dashboard page.
+- lazy load diffs on merge request discussions.
+- Bulk deleting refs is handled by Gitaly by default.
+- ListCommitsByOid is executed by Gitaly by default.
+
+### Added (38 changes, 7 of them are from the community)
+
+- Add HTTPS-only pages. !16273 (rfwatson)
+- adds closed by informations in issue api. !17042 (haseebeqx)
+- Projects and groups badges settings UI. !17114
+- Add per-runner configured job timeout. !17221
+- Add alternate archive route for simplified packaging. !17225
+- Add support for pipeline variables expressions in only/except. !17316
+- Add object storage support for LFS objects, CI artifacts, and uploads. !17358
+- Added confirmation modal for changing username. !17405
+- Implement foreground verification of CI artifacts. !17578
+- Extend API for exporting a project with direct upload URL. !17686
+- Move ci/lint under project's namespace. !17729
+- Add Total CPU/Memory consumption metrics for Kubernetes. !17731
+- Adds the option to the project export API to override the project description and display GitLab export description once imported. !17744
+- Port direct upload of LFS artifacts from EE. !17752
+- Adds support for OmniAuth JWT provider. !17774
+- Display error message on job's tooltip if this one fails. !17782
+- Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users. !17860 (Elias Werberich)
+- Extend API for importing a project export with overwrite support. !17883
+- Create Deploy Tokens to allow permanent access to repository and registry. !17894
+- Detect commit message trailers and link users properly to their accounts on Gitlab. !17919 (cousine)
+- Adds cancel btn to new pages domain page. !18026 (Jacopo Beschi @jacopo-beschi)
+- API: Add parameter merge_method to projects. !18031 (Jan Beckmann)
+- Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436. !18036
+- Allow overriding params on project import through API. !18086
+- Support LFS objects when importing/exporting GitLab project archives. !18115
+- Store sha256 checksum of artifact metadata. !18149
+- Limit the number of failed logins when using LDAP for authentication. !43525
+- Allow assigning and filtering issuables by ancestor group labels.
+- Include subgroup issues when searching for group issues using the API.
+- Allow to store uploads by default on Object Storage.
+- Add slash command for moving issues. (Adam Pahlevi)
+- Render MR commit SHA instead "diffs" when viable.
+- Send @mention notifications even if a user has explicitly unsubscribed from item.
+- Add support for Sidekiq JSON logging.
+- Add Gitaly call details to performance bar.
+- Add support for patch link extension for commit links on GitLab Flavored Markdown.
+- Allow feature gates to be removed through the API.
+- Allow merge requests related to a commit to be found via API.
+
+### Other (27 changes, 11 of them are from the community)
+
+- Send notification emails when push to a merge request. !7610 (YarNayar)
+- Rename modal.vue to deprecated_modal.vue. !17438
+- Atomic generation of internal ids for issues. !17580
+- Use object ID to prevent duplicate keys Vue warning on Issue Boards page during development. !17682
+- Update foreman from 0.78.0 to 0.84.0. !17690 (Takuya Noguchi)
+- Add realtime pipeline status for adding/viewing files. !17705
+- Update documentation to reflect current minimum required versions of node and yarn. !17706
+- Update knapsack to 1.16.0. !17735 (Takuya Noguchi)
+- Update CI services documnetation. !17749
+- Added i18n support for the prometheus memory widget. !17753
+- Use specific names for filtered CI variable controller parameters. !17796
+- Apply NestingDepth (level 5) (framework/dropdowns.scss). !17820 (Takuya Noguchi)
+- Clean up selectors in framework/header.scss. !17822 (Takuya Noguchi)
+- Bump `state_machines-activerecord` to 0.5.1. !17924 (blackst0ne)
+- Increase the memory limits used in the unicorn killer. !17948
+- Replace the spinach test with an rspec analog. !17950 (blackst0ne)
+- Remove unused index from events table. !18014
+- Make all workhorse gitaly calls opt-out, take 2. !18043
+- Update brakeman 3.6.1 to 4.2.1. !18122 (Takuya Noguchi)
+- Replace the `project/issues/labels.feature` spinach test with an rspec analog. !18126 (blackst0ne)
+- Bump html-pipeline to 2.7.1. !18132 (@blackst0ne)
+- Remove test_ci rake task. !18139 (Takuya Noguchi)
+- Add documentation for Pipelines failure reasons. !18352
+- Improve JIRA event descriptions.
+- Add query counts to profiler output.
+- Move Sidekiq exporter logs to log/sidekiq_exporter.log.
+- Upgrade Gitaly to upgrade its charlock_holmes.
+
+
+## 10.6.5 (2018-04-24)
+
+### Security (1 change)
+
+- Sanitizes user name to avoid XSS attacks.
+
+
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)
@@ -243,6 +705,13 @@ entry.
- Use host URL to build JIRA remote link icon.
+## 10.5.8 (2018-04-24)
+
+### Security (1 change)
+
+- Sanitizes user name to avoid XSS attacks.
+
+
## 10.5.7 (2018-04-03)
### Security (2 changes)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9c8fdc1275b..383d13656e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,6 +9,10 @@ terms.
[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md)
+All Documentation content that resides under the [doc/ directory](/doc) of this
+repository is licensed under Creative Commons:
+[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
+
_This notice should stay as the first item in the CONTRIBUTING.md file._
---
@@ -25,10 +29,12 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- - [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
+ - [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
+ - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
+ - [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4)
+ - [Severity labels (~S1, ~S2, ~S3 , ~S4)](#bug-severity-labels-s1-s2-s3-s4)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
-- [Implement design & UI elements](#implement-design-ui-elements)
+- [Implement design & UI elements](#implement-design--ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
@@ -112,7 +118,7 @@ is a great place to start. Issues with a lower weight (1 or 2) are deemed
suitable for beginners. These issues will be of reasonable size and challenge,
for anyone to start contributing to GitLab. If you have any questions or need help visit [Getting Help](https://about.gitlab.com/getting-help/#discussion) to
learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
-please consider we favor
+please consider we favor
[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
## Workflow labels
@@ -125,8 +131,10 @@ 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", ~Discussion, ~Edge, ~Platform, etc.
-- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
+- Team: ~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.
+- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
+- Priority: ~P1, ~P2, ~P3, ~P4
+- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the
[labels page][labels-page].
@@ -160,20 +168,20 @@ hits. They are not always necessary, but very convenient.
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
-issue is labelled with a subject label corresponding to your expertise.
+issue is labeled with a subject label corresponding to your expertise.
Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
Subject labels are always all-lowercase.
-### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)
+### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
-The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
+The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
@@ -185,34 +193,56 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
-### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
+### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
-Priority labels help us clearly communicate expectations of the work for the
-release. There are two levels of priority labels:
+Milestone labels help us clearly communicate expectations of the work for the
+release. There are three levels of Milestone labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
- ~Stretch: Issues that are a stretch goal for delivering in the current
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
-- ~"Next Patch Release": Issues to put in the next patch release. Work on these
+- ~"Next Patch Release": Issues to put in the next patch release. Work on these
first, and add the "Pick Into X" label to the merge request, along with the
appropriate milestone.
Each issue scheduled for the current milestone should be labeled ~Deliverable
-or ~"Stretch". Any open issue for a previous milestone should be labeled
+or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
-### Severity labels (~S1, ~S2, etc.)
+### Bug Priority labels (~P1, ~P2, ~P3, ~P4)
+
+Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
+If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
+This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
+
+| Label | Meaning | Estimate time to fix | Guidance |
+|-------|-----------------|------------------------------------------------------------------|----------|
+| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
+| ~P2 | High Priority | The next release | |
+| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
+| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
+
+### Bug Severity labels (~S1, ~S2, ~S3, ~S4)
Severity labels help us clearly communicate the impact of a ~bug on users.
-| Label | Meaning | Example |
-|-------|------------------------------------------|---------|
-| ~S1 | Feature broken, no workaround | Unable to create an issue |
-| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
-| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
-| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
+| Label | Meaning | Impact of the defect | Example |
+|-------|-------------------|-------------------------------------------------------|---------|
+| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
+| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
+| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
+| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
+
+#### Severity impact guidance
+
+| Label | Security Impact | Availability / Performance Impact |
+|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
+| ~S1 | >50% users impacted (possible company extinction level event) | |
+| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
+| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
+| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
### Label for community contributors (~"Accepting Merge Requests")
@@ -266,7 +296,24 @@ any potential community contributor to @-mention per above.
## Implement design & UI elements
-Please see the [UX Guide for GitLab].
+For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
+
+The UX team uses labels to manage their workflow.
+
+The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
+To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook.
+
+Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue.
+
+The UX team has a special type label called ~"design artifact". This label indicates that the final output
+for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
+Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
+needed until the solution has been decided.
+
+~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.
+
+Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the
+existing issue or decide to create a whole new issue for the purpose of development.
## Issue tracker
@@ -690,7 +737,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md
[us-english]: https://en.wikipedia.org/wiki/American_English
-
-[^1]: Please note that specs other than JavaScript specs are considered backend
- code.
- \ No newline at end of file
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5f8cbfdb7d7..89eba2c5b85 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.95.0
+0.103.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index a3df0a6959e..f374f6662e9 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.8.0
+0.9.1
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index ee74734aa22..fae6e3d04b2 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-4.1.0
+4.2.1
diff --git a/Gemfile b/Gemfile
index 1c822a40fc6..b6b82bad8a4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,7 @@ 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.6' : '4.2.10'
+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 ---
@@ -33,7 +33,7 @@ gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12'
# Authentication libraries
-gem 'devise', '~> 4.2'
+gem 'devise', '~> 4.4'
gem 'doorkeeper', '~> 4.3'
gem 'doorkeeper-openid_connect', '~> 1.3'
gem 'omniauth', '~> 1.8'
@@ -41,7 +41,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
-gem 'omniauth-github', '~> 1.1.1'
+gem 'omniauth-github', '~> 1.3'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
@@ -50,8 +50,7 @@ gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
-gem 'omniauth-authentiq', '~> 0.3.1'
-gem 'omniauth-jwt', '~> 0.0.2'
+gem 'omniauth-authentiq', '~> 0.3.3'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
@@ -82,7 +81,7 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
-gem 'gitlab-gollum-lib', '~> 4.2'
+gem 'gitlab-gollum-lib', '~> 4.2', require: false
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
@@ -91,7 +90,7 @@ gem 'github-linguist', '~> 5.3.3', require: 'linguist'
# API
gem 'grape', '~> 1.0'
-gem 'grape-entity', '~> 0.6.0'
+gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# Disable strong_params so that Mash does not respond to :permitted?
@@ -140,7 +139,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.6'
gem 'asciidoctor-plantuml', '0.0.8'
-gem 'rouge', '~> 2.0'
+gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2'
@@ -161,9 +160,9 @@ gem 'state_machines-activerecord', '~> 0.5.1'
gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs
-gem 'sidekiq', '~> 5.0'
+gem 'sidekiq', '~> 5.1'
gem 'sidekiq-cron', '~> 0.6.0'
-gem 'redis-namespace', '~> 1.5.2'
+gem 'redis-namespace', '~> 1.6.0'
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
# Cron Parser
@@ -175,6 +174,9 @@ gem 'httparty', '~> 0.13.3'
# Colored output to console
gem 'rainbow', '~> 2.2'
+# Progress bar
+gem 'ruby-progressbar'
+
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
@@ -185,6 +187,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0'
+# User agent parsing
+gem 'device_detector'
+
# Cache
gem 'redis-rails', '~> 5.0.2'
@@ -216,9 +221,6 @@ gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration
gem 'kubeclient', '~> 3.0'
-# d3
-gem 'd3_rails', '~> 3.5.0'
-
# Sanitize user input
gem 'sanitize', '~> 2.0'
gem 'babosa', '~> 1.0.2'
@@ -255,10 +257,9 @@ gem 'sass-rails', '~> 5.0.6'
gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.5.2'
-gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
-gem 'gon', '~> 6.1.0'
+gem 'gon', '~> 6.2'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'request_store', '~> 1.3'
gem 'select2-rails', '~> 3.5.9'
@@ -283,7 +284,6 @@ gem 'batch-loader', '~> 1.2.1'
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
-gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres
gem 'peek-rblineprof', '~> 0.2.0'
gem 'peek-redis', '~> 1.2.0'
@@ -296,7 +296,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~> 0.9.1'
+ gem 'prometheus-client-mmap', '~> 0.9.3'
gem 'raindrops', '~> 0.18'
end
@@ -327,8 +327,6 @@ group :development, :test do
gem 'factory_bot_rails', '~> 4.8.2'
gem 'rspec-rails', '~> 3.6.0'
gem 'rspec-retry', '~> 0.4.5'
- gem 'spinach-rails', '~> 0.2.1'
- gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized', require: false
@@ -345,7 +343,6 @@ group :development, :test do
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'spring-commands-spinach', '~> 1.1.0'
gem 'gitlab-styles', '~> 2.3', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
@@ -415,8 +412,8 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
-gem 'grpc', '~> 1.10.0'
+gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly'
+gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
@@ -433,6 +430,4 @@ gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
# Asset synchronization
-gem 'asset_sync', '~> 2.2.0'
-
-gem 'goldiloader', '~> 2.0'
+gem 'asset_sync', '~> 2.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7f243491c90..b0b7bb537a8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -59,7 +59,7 @@ GEM
asciidoctor (1.5.6.2)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.2.0)
+ asset_sync (2.4.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
@@ -69,9 +69,6 @@ GEM
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.0)
- autoprefixer-rails (6.2.3)
- execjs
- json
awesome_print (1.2.0)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
@@ -91,9 +88,6 @@ GEM
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
- bootstrap-sass (3.3.6)
- autoprefixer-rails (>= 5.2.1)
- sass (>= 3.3.4)
bootstrap_form (2.7.0)
brakeman (4.2.1)
browser (2.2.0)
@@ -131,7 +125,6 @@ GEM
coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- colorize (0.7.7)
commonmarker (0.17.8)
ruby-enum (~> 0.5)
concord (0.1.5)
@@ -143,12 +136,10 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.3)
+ crass (1.0.4)
creole (0.5.0)
css_parser (1.5.0)
addressable
- d3_rails (3.5.11)
- railties (>= 3.1.0)
daemons (1.2.3)
database_cleaner (1.5.3)
debug_inspector (0.0.2)
@@ -161,10 +152,11 @@ GEM
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devise (4.2.0)
+ device_detector (1.0.0)
+ devise (4.4.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
- railties (>= 4.1.0, < 5.1)
+ railties (>= 4.1.0, < 6.0)
responders
warden (~> 1.2.3)
devise-two-factor (3.0.0)
@@ -178,7 +170,7 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (4.3.1)
+ doorkeeper (4.3.2)
railties (>= 4.2)
doorkeeper-openid_connect (1.3.0)
doorkeeper (~> 4.3)
@@ -196,7 +188,7 @@ GEM
et-orbi (1.0.3)
tzinfo
eventmachine (1.0.8)
- excon (0.60.0)
+ excon (0.62.0)
execjs (2.6.0)
expression_parser (0.9.0)
factory_bot (4.8.2)
@@ -289,10 +281,9 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gherkin-ruby (0.3.2)
- gitaly-proto (0.94.0)
+ gitaly-proto (0.100.0)
google-protobuf (~> 3.1)
- grpc (~> 1.0)
+ grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@@ -303,12 +294,12 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-gollum-lib (4.2.7.1)
+ gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
- rouge (~> 2.1)
+ rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
@@ -331,14 +322,10 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
- goldiloader (2.0.1)
- activerecord (>= 4.2, < 5.2)
- activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gon (6.1.0)
+ gon (6.2.0)
actionpack (>= 3.0)
- json
multi_json
request_store (>= 1.0)
google-api-client (0.19.8)
@@ -368,8 +355,8 @@ GEM
rack (>= 1.3.0)
rack-accept
virtus (>= 1.0.0)
- grape-entity (0.6.0)
- activesupport
+ grape-entity (0.7.1)
+ activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-route-helpers (2.1.0)
activesupport
@@ -377,7 +364,7 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.10.0)
+ grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
@@ -486,10 +473,11 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
- lograge (0.5.1)
- actionpack (>= 4, < 5.2)
- activesupport (>= 4, < 5.2)
- railties (>= 4, < 5.2)
+ lograge (0.10.0)
+ actionpack (>= 4)
+ activesupport (>= 4)
+ railties (>= 4)
+ request_store (~> 1.0)
loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@@ -535,8 +523,9 @@ GEM
rack (>= 1.6.2, < 3)
omniauth-auth0 (2.0.0)
omniauth-oauth2 (~> 1.4)
- omniauth-authentiq (0.3.1)
- omniauth-oauth2 (~> 1.3, >= 1.3.1)
+ omniauth-authentiq (0.3.3)
+ jwt (>= 1.5)
+ omniauth-oauth2 (>= 1.5)
omniauth-azure-oauth2 (0.0.9)
jwt (~> 1.0)
omniauth (~> 1.0)
@@ -547,9 +536,9 @@ GEM
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
- omniauth-github (1.1.2)
- omniauth (~> 1.0)
- omniauth-oauth2 (~> 1.1)
+ omniauth-github (1.3.0)
+ omniauth (~> 1.5)
+ omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
@@ -557,9 +546,6 @@ GEM
jwt (>= 1.5)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5)
- omniauth-jwt (0.0.2)
- jwt
- omniauth (~> 1.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
@@ -605,8 +591,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
- peek-performance_bar (1.3.1)
- peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
@@ -640,7 +624,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.9.1)
+ prometheus-client-mmap (0.9.3)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -652,7 +636,7 @@ GEM
pry (>= 0.9.10)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
- rack (1.6.9)
+ rack (1.6.10)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -700,7 +684,7 @@ GEM
rainbow (2.2.2)
rake
raindrops (0.18.0)
- rake (12.3.0)
+ rake (12.3.1)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
@@ -725,8 +709,8 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
- redis-namespace (1.5.2)
- redis (~> 3.0, >= 3.0.4)
+ redis-namespace (1.6.0)
+ redis (>= 3.0.4)
redis-rack (2.0.4)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
@@ -741,8 +725,9 @@ GEM
declarative-option (< 0.2.0)
uber (< 0.2.0)
request_store (1.3.1)
- responders (2.3.0)
- railties (>= 4.2.0, < 5.1)
+ responders (2.4.0)
+ actionpack (>= 4.2.0, < 5.3)
+ railties (>= 4.2.0, < 5.3)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
@@ -750,7 +735,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.2.1)
+ rouge (3.1.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -854,11 +839,11 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.0.5)
+ sidekiq (5.1.3)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
- redis (>= 3.3.4, < 5)
+ redis (>= 3.3.5, < 5)
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
@@ -877,22 +862,10 @@ GEM
simplecov-html (0.10.0)
slack-notifier (1.5.1)
slop (3.6.0)
- spinach (0.8.10)
- colorize
- gherkin-ruby (>= 0.3.2)
- json
- spinach-rails (0.2.1)
- capybara (>= 2.0.0)
- railties (>= 3)
- spinach (>= 0.4)
- spinach-rerun-reporter (0.0.2)
- spinach (~> 0.8)
spring (2.0.1)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- spring-commands-spinach (1.1.0)
- spring (>= 0.9.1)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@@ -945,7 +918,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
- unicode-display_width (1.3.0)
+ unicode-display_width (1.3.2)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -972,7 +945,7 @@ GEM
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
vmstat (2.3.0)
- warden (1.2.6)
+ warden (1.2.7)
rack (>= 1.0)
webmock (2.3.2)
addressable (>= 2.3.6)
@@ -1003,7 +976,7 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.2.0)
+ asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -1013,7 +986,6 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
- bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
browser (~> 2.2)
@@ -1029,11 +1001,11 @@ DEPENDENCIES
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
- d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0)
- devise (~> 4.2)
+ device_detector
+ devise (~> 4.4)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
@@ -1064,7 +1036,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.94.0)
+ gitaly-proto (~> 0.100.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@@ -1072,16 +1044,15 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- goldiloader (~> 2.0)
- gon (~> 6.1.0)
+ gon (~> 6.2)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
- grape-entity (~> 0.6.0)
+ grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
- grpc (~> 1.10.0)
+ grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@@ -1115,14 +1086,13 @@ DEPENDENCIES
octokit (~> 4.8)
omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0)
- omniauth-authentiq (~> 0.3.1)
+ omniauth-authentiq (~> 0.3.3)
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
- omniauth-github (~> 1.1.1)
+ omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3)
- omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
@@ -1133,14 +1103,13 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
- peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.9.1)
+ prometheus-client-mmap (~> 0.9.3)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1160,11 +1129,11 @@ DEPENDENCIES
recaptcha (~> 3.0)
redcarpet (~> 3.4)
redis (~> 3.2)
- redis-namespace (~> 1.5.2)
+ redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
- rouge (~> 2.0)
+ rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
@@ -1175,6 +1144,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
+ ruby-progressbar
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
@@ -1188,17 +1158,14 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 3.1.2)
- sidekiq (~> 5.0)
+ sidekiq (~> 5.1)
sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4)
simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
- spinach-rails (~> 0.2.1)
- spinach-rerun-reporter (~> 0.0.2)
spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
- spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
stackprof (~> 0.2.10)
@@ -1224,4 +1191,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.1
+ 1.16.2
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index c953b9708a0..af7305619eb 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -4,43 +4,43 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.4)
- actioncable (5.0.6)
- actionpack (= 5.0.6)
+ actioncable (5.0.7)
+ actionpack (= 5.0.7)
nio4r (>= 1.2, < 3.0)
websocket-driver (~> 0.6.1)
- actionmailer (5.0.6)
- actionpack (= 5.0.6)
- actionview (= 5.0.6)
- activejob (= 5.0.6)
+ 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 (~> 2.0)
- actionpack (5.0.6)
- actionview (= 5.0.6)
- activesupport (= 5.0.6)
+ 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 (5.0.6)
- activesupport (= 5.0.6)
+ actionview (5.0.7)
+ activesupport (= 5.0.7)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.0.6)
- activesupport (= 5.0.6)
+ activejob (5.0.7)
+ activesupport (= 5.0.7)
globalid (>= 0.3.6)
- activemodel (5.0.6)
- activesupport (= 5.0.6)
- activerecord (5.0.6)
- activemodel (= 5.0.6)
- activesupport (= 5.0.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.6)
+ activesupport (5.0.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (~> 0.7)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
acts-as-taggable-on (5.0.0)
@@ -62,14 +62,14 @@ GEM
asciidoctor (1.5.6.1)
asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
- asset_sync (2.2.0)
+ asset_sync (2.4.0)
activemodel (>= 4.1.0)
fog-core
mime-types (>= 2.99)
unf
ast (2.4.0)
- atomic (1.1.100)
- attr_encrypted (3.0.3)
+ atomic (1.1.99)
+ attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (8.1.0.1)
@@ -132,7 +132,6 @@ GEM
coderay (1.1.2)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
- colorize (0.8.1)
commonmarker (0.17.9)
ruby-enum (~> 0.5)
concord (0.1.5)
@@ -144,12 +143,10 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.3)
+ crass (1.0.4)
creole (0.5.0)
css_parser (1.6.0)
addressable
- d3_rails (3.5.17)
- railties (>= 3.1.0)
daemons (1.2.6)
database_cleaner (1.5.3)
debug_inspector (0.0.3)
@@ -162,6 +159,7 @@ GEM
activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
+ device_detector (1.0.1)
devise (4.4.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@@ -290,10 +288,9 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gherkin-ruby (0.3.2)
- gitaly-proto (0.94.0)
+ gitaly-proto (0.99.0)
google-protobuf (~> 3.1)
- grpc (~> 1.0)
+ grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@@ -304,6 +301,17 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
+ gitlab-gollum-lib (4.2.7.2)
+ gemojione (~> 3.2)
+ github-markup (~> 1.6)
+ gollum-grit_adapter (~> 1.0)
+ nokogiri (>= 1.6.1, < 2.0)
+ rouge (~> 3.1)
+ sanitize (~> 2.1)
+ stringex (~> 2.6)
+ gitlab-gollum-rugged_adapter (0.4.4)
+ mime-types (>= 1.15)
+ rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
@@ -321,25 +329,10 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
- goldiloader (2.0.1)
- activerecord (>= 4.2, < 5.2)
- activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.2.7)
- gemojione (~> 3.2)
- github-markup (~> 1.6)
- gollum-grit_adapter (~> 1.0)
- nokogiri (>= 1.6.1, < 2.0)
- rouge (~> 2.1)
- sanitize (~> 2.1)
- stringex (~> 2.6)
- gollum-rugged_adapter (0.4.4)
- mime-types (>= 1.15)
- rugged (~> 0.25)
- gon (6.1.0)
+ gon (6.2.0)
actionpack (>= 3.0)
- json
multi_json
request_store (>= 1.0)
google-api-client (0.19.8)
@@ -369,8 +362,8 @@ GEM
rack (>= 1.3.0)
rack-accept
virtus (>= 1.0.0)
- grape-entity (0.6.1)
- activesupport (>= 5.0.0)
+ grape-entity (0.7.1)
+ activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-route-helpers (2.1.0)
activesupport
@@ -378,7 +371,7 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.10.0)
+ grpc (1.11.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
@@ -422,7 +415,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (0.9.5)
+ i18n (1.0.1)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
influxdb (0.5.3)
@@ -517,7 +510,7 @@ GEM
net-ldap (0.16.1)
net-ssh (4.2.0)
netrc (0.11.0)
- nio4r (2.2.0)
+ nio4r (2.3.1)
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
numerizer (0.1.1)
@@ -547,9 +540,9 @@ GEM
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
- omniauth-github (1.1.2)
- omniauth (~> 1.0)
- omniauth-oauth2 (~> 1.1)
+ omniauth-github (1.3.0)
+ omniauth (~> 1.5)
+ omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.3)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
@@ -557,9 +550,6 @@ GEM
jwt (>= 1.5)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5)
- omniauth-jwt (0.0.2)
- jwt
- omniauth (~> 1.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
@@ -605,8 +595,6 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
- peek-performance_bar (1.3.1)
- peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
concurrent-ruby-ext
@@ -640,7 +628,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.9.1)
+ prometheus-client-mmap (0.9.2)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
@@ -651,7 +639,7 @@ GEM
pry (>= 0.10.4)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
- rack (2.0.4)
+ rack (2.0.5)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (4.4.1)
@@ -669,17 +657,17 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (5.0.6)
- actioncable (= 5.0.6)
- actionmailer (= 5.0.6)
- actionpack (= 5.0.6)
- actionview (= 5.0.6)
- activejob (= 5.0.6)
- activemodel (= 5.0.6)
- activerecord (= 5.0.6)
- activesupport (= 5.0.6)
+ 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.6)
+ railties (= 5.0.7)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -690,21 +678,21 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
- rails-html-sanitizer (1.0.3)
- loofah (~> 2.0)
+ 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.6)
- actionpack (= 5.0.6)
- activesupport (= 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 (2.2.2)
rake
raindrops (0.19.0)
- rake (12.3.0)
+ rake (12.3.1)
rb-fsevent (0.10.3)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
@@ -755,7 +743,7 @@ GEM
retriable (3.1.1)
rinku (2.0.4)
rotp (2.1.2)
- rouge (2.2.1)
+ rouge (3.1.1)
rqrcode (0.10.1)
chunky_png (~> 1.0)
rqrcode-rails3 (0.1.7)
@@ -881,22 +869,10 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-notifier (1.5.1)
- spinach (0.8.10)
- colorize
- gherkin-ruby (>= 0.3.2)
- json
- spinach-rails (0.2.1)
- capybara (>= 2.0.0)
- railties (>= 3)
- spinach (>= 0.4)
- spinach-rerun-reporter (0.0.2)
- spinach (~> 0.8)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- spring-commands-spinach (1.1.0)
- spring (>= 0.9.1)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@@ -1008,8 +984,8 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
- asset_sync (~> 2.2.0)
- attr_encrypted (~> 3.0.0)
+ asset_sync (~> 2.4)
+ attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@@ -1034,11 +1010,11 @@ DEPENDENCIES
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
- d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5)
- devise (~> 4.2)
+ device_detector
+ devise (~> 4.4)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.3)
@@ -1069,24 +1045,23 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.94.0)
+ gitaly-proto (~> 0.99.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
+ gitlab-gollum-lib (~> 4.2)
+ gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
- goldiloader (~> 2.0)
- gollum-lib (~> 4.2)
- gollum-rugged_adapter (~> 0.4.4)
- gon (~> 6.1.0)
+ gon (~> 6.2)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
- grape-entity (~> 0.6.0)
+ grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
- grpc (~> 1.10.0)
+ grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@@ -1124,10 +1099,9 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
- omniauth-github (~> 1.1.1)
+ omniauth-github (~> 1.3)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3)
- omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
@@ -1138,21 +1112,20 @@ DEPENDENCIES
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0)
- peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0)
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.9.1)
+ prometheus-client-mmap (~> 0.9.2)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0)
- rails (= 5.0.6)
+ rails (= 5.0.7)
rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1)
@@ -1170,7 +1143,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
- rouge (~> 2.0)
+ rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)
@@ -1200,11 +1173,8 @@ DEPENDENCIES
simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
- spinach-rails (~> 0.2.1)
- spinach-rerun-reporter (~> 0.0.2)
spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
- spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
stackprof (~> 0.2.10)
diff --git a/LICENSE b/LICENSE
index 15c423e1416..a76372fad2c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,25 +1,7 @@
-Copyright (c) 2011-2017 GitLab B.V.
+Copyright GitLab B.V.
-With regard to the GitLab Software:
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-For all third party components incorporated into the GitLab Software, those
-components are licensed under the original license provided by the owner of the
-applicable component. \ No newline at end of file
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
index 9ead6d51c5d..0266fe82c82 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
-[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
@@ -67,6 +66,14 @@ You can access a new installation with the login **`root`** and password **`5ive
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
+## Licensing
+
+GitLab Community Edition (CE) is available freely under the MIT Expat license.
+
+All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
+
+All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
+
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
diff --git a/VERSION b/VERSION
index 7a86eda5728..8ca9077d87b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-10.7.0-pre
+11.0.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 8ad3d18b302..ce1069276ab 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -21,8 +21,11 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
+ commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
+ pipelinesPath: '/api/:version/projects/:id/pipelines',
+ pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -164,6 +167,19 @@ const Api = {
});
},
+ commitPipelines(projectId, sha) {
+ const encodedProjectId = projectId
+ .split('/')
+ .map(fragment => encodeURIComponent(fragment))
+ .join('/');
+
+ const url = Api.buildUrl(Api.commitPipelinesPath)
+ .replace(':project_id', encodedProjectId)
+ .replace(':sha', encodeURIComponent(sha));
+
+ return axios.get(url);
+ },
+
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
@@ -222,6 +238,20 @@ const Api = {
});
},
+ pipelines(projectPath, params = {}) {
+ const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url, { params });
+ },
+
+ pipelineJobs(projectPath, pipelineId, params = {}) {
+ const url = Api.buildUrl(this.pipelineJobsPath)
+ .replace(':id', encodeURIComponent(projectPath))
+ .replace(':pipeline_id', pipelineId);
+
+ return axios.get(url, { params });
+ },
+
buildUrl(url) {
let urlRoot = '';
if (gon.relative_url_root != null) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 976d32abe9b..eb0f06efab4 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -345,7 +345,7 @@ class AwardsHandler {
counter.text(counterNumber - 1);
this.removeYouFromUserList($emojiButton);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
- $emojiButton.tooltip('destroy');
+ $emojiButton.tooltip('dispose');
counter.text('0');
this.removeYouFromUserList($emojiButton);
if ($emojiButton.parents('.note').length) {
@@ -358,7 +358,7 @@ class AwardsHandler {
}
removeEmoji($emojiButton) {
- $emojiButton.tooltip('destroy');
+ $emojiButton.tooltip('dispose');
$emojiButton.remove();
const $votesBlock = this.getVotesBlock();
if ($votesBlock.find('.js-emoji-btn').length === 0) {
@@ -392,7 +392,7 @@ class AwardsHandler {
.removeAttr('data-title')
.removeAttr('data-original-title')
.attr('title', this.toSentence(authors))
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
addYouToUserList(votesBlock, emoji) {
@@ -405,7 +405,7 @@ class AwardsHandler {
users.unshift('You');
return awardBlock
.attr('title', this.toSentence(users))
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
createAwardButtonForVotesBlock(votesBlock, emojiName) {
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 6e6cb31e3ac..d0f60e1d4cb 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -89,7 +89,7 @@ export default {
v-show="hasError"
class="btn-group"
>
- <div class="btn btn-default btn-xs disabled">
+ <div class="btn btn-default btn-sm disabled">
<icon
class="prepend-left-8 append-right-8"
name="doc_image"
@@ -98,7 +98,7 @@ export default {
/>
</div>
<div
- class="btn btn-default btn-xs disabled"
+ class="btn btn-default btn-sm disabled"
>
<span class="prepend-left-8 append-right-8">{{ s__('Badges|No badge image') }}</span>
</div>
@@ -106,7 +106,7 @@ export default {
<button
v-show="hasError"
- class="btn btn-transparent btn-xs text-primary"
+ class="btn btn-transparent btn-sm text-primary"
type="button"
v-tooltip
:title="s__('Badges|Reload badge image')"
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index ca7197e1e0f..268968b63b3 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -23,8 +23,8 @@ export default {
</script>
<template>
- <div class="panel panel-default">
- <div class="panel-heading">
+ <div class="card">
+ <div class="card-header">
{{ s__('Badges|Your badges') }}
<span
v-show="!isLoading"
@@ -33,19 +33,19 @@ export default {
</div>
<loading-icon
v-show="isLoading"
- class="panel-body"
+ class="card-body"
size="2"
/>
<div
v-if="hasNoBadges"
- class="panel-body"
+ class="card-body"
>
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
</div>
<div
v-else
- class="panel-body"
+ class="card-body"
>
<badge-list-row
v-for="badge in badges"
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index e2a73a1797c..75834ba351d 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -8,10 +8,10 @@ function showTooltip(target, title) {
if (!$target.data('hideTooltip')) {
$target
.attr('title', title)
- .tooltip('fixTitle')
+ .tooltip('_fixTitle')
.tooltip('show')
.attr('title', originalTitle)
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
}
}
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 7e98e04303a..56293d5f96f 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
- const {
- name,
- unicodeVersion,
- fallbackSrc,
- fallbackSpriteClass,
- } = this.dataset;
+ const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
- const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
- this.childNodes,
- childNode => childNode.nodeType === 3,
- );
+ const isEmojiUnicode =
+ this.childNodes &&
+ Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
- if (
- emojiUnicode &&
- isEmojiUnicode &&
- !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
- ) {
+ if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
+ if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
+ const emojiSpriteLinkTag = document.createElement('link');
+ emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
+ emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
+ document.head.appendChild(emojiSpriteLinkTag);
+ gon.emoji_sprites_css_added = true;
+ }
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 3ec932bdb73..b6e2781773c 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -69,7 +69,7 @@ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-q
$this.tooltip({
container: 'body',
html: 'true',
- placement: 'auto top',
+ placement: 'top',
title,
trigger: 'manual',
});
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index ffff4ddb71a..a8b6dbf0948 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -42,9 +42,9 @@ $.fn.requiresInput = function requiresInput() {
function hideOrShowHelpBlock(form) {
const selected = $('.js-select-namespace option:selected');
if (selected.length && selected.data('optionsParent') === 'groups') {
- form.find('.help-block').hide();
+ form.find('.form-text.text-muted').hide();
} else if (selected.length) {
- form.find('.help-block').show();
+ form.find('.form-text.text-muted').show();
}
}
diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index c17877a276d..766039404ce 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -2,9 +2,9 @@ import sqljs from 'sql.js';
import { template as _template } from 'underscore';
const PREVIEW_TEMPLATE = _template(`
- <div class="panel panel-default">
- <div class="panel-heading"><%- name %></div>
- <div class="panel-body">
+ <div class="card">
+ <div class="card-header"><%- name %></div>
+ <div class="card-body">
<img class="img-thumbnail" src="data:image/png;base64,<%- image %>"/>
</div>
</div>
diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js
index 0799991aa40..13318c58006 100644
--- a/app/assets/javascripts/blob/sketch/index.js
+++ b/app/assets/javascripts/blob/sketch/index.js
@@ -44,7 +44,7 @@ export default class SketchLoader {
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
- previewImage.className = 'img-responsive';
+ previewImage.className = 'img-fluid';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 137e1f5a099..f61c0be9230 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -116,7 +116,7 @@ export default class BlobViewer {
this.copySourceBtn.classList.add('disabled');
}
- $(this.copySourceBtn).tooltip('fixTitle');
+ $(this.copySourceBtn).tooltip('_fixTitle');
}
switchToViewer(name) {
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 84885ca9306..33e3369b971 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -77,7 +77,7 @@ export default {
<template>
<li
- class="card"
+ class="board-card"
:class="{
'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id,
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 8d84c1735b8..e8dfd95f7ae 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -92,7 +92,7 @@ export default {
<template>
<div class="board-new-issue-form">
- <div class="card">
+ <div class="board-card">
<form @submit="submit($event)">
<div
class="flash-container"
@@ -122,7 +122,7 @@ export default {
/>
<div class="clearfix prepend-top-10">
<button
- class="btn btn-success pull-left"
+ class="btn btn-success float-left"
type="submit"
:disabled="disabled"
ref="submit-button"
@@ -130,7 +130,7 @@ export default {
Submit issue
</button>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="cancel"
>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 84fe9b1288a..dcc07810d01 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -135,8 +135,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({
},
template: `
<div>
- <div class="card-header">
- <h4 class="card-title">
+ <div class="board-card-header">
+ <h4 class="board-card-title">
<i
class="fa fa-eye-slash confidential-icon"
v-if="issue.confidential"
@@ -147,13 +147,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
:href="issue.path"
:title="issue.title">{{ issue.title }}</a>
<span
- class="card-number"
+ class="board-card-number"
v-if="issueId"
>
{{ issue.referencePath }}
</span>
</h4>
- <div class="card-assignee">
+ <div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
:key="assignee.id"
@@ -175,11 +175,11 @@ gl.issueBoards.IssueCardInner = Vue.extend({
</div>
</div>
<div
- class="card-footer"
+ class="board-card-footer"
v-if="showLabelFooter"
>
<button
- class="label color-label has-tooltip"
+ class="badge color-label has-tooltip"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 9e37f95cdd6..eb8a66975ee 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
template: `
<section class="empty-state">
<div class="row">
- <div class="col-xs-12 col-sm-6 col-sm-push-6">
+ <div class="col-xs-12 col-sm-6 order-sm-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
- <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+ <div class="col-xs-12 col-sm-6 order-sm-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 9735e0ddacc..11bb3e98334 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -58,7 +58,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
template: `
<footer
class="form-actions add-issues-footer">
- <div class="pull-left">
+ <div class="float-left">
<button
class="btn btn-success"
type="button"
@@ -72,7 +72,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
<lists-dropdown></lists-dropdown>
</div>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="toggleModal(false)">
Cancel
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index 6b04a6c7a6c..6c662432037 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -133,9 +133,9 @@ gl.issueBoards.ModalList = Vue.extend({
<div
v-for="issue in group"
v-if="showIssue(issue)"
- class="card-parent">
+ class="board-card-parent">
<div
- class="card"
+ class="board-card"
:class="{ 'is-active': issue.selected }"
@click="toggleIssue($event, issue)">
<issue-card-inner
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index b6465a88e5e..9d331de8e22 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -24,7 +24,7 @@ gl.issueBoards.ModalTabs = Vue.extend({
role="button"
@click.prevent="changeTab('all')">
Open issues
- <span class="badge">
+ <span class="badge badge-pill">
{{ issuesCount }}
</span>
</a>
@@ -35,7 +35,7 @@ gl.issueBoards.ModalTabs = Vue.extend({
role="button"
@click.prevent="changeTab('selected')">
Selected issues
- <span class="badge">
+ <span class="badge badge-pill">
{{ selectedCount }}
</span>
</a>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a6f8681cfac..29ab13b8e0b 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -214,7 +214,7 @@ export default () => {
if (this.disabled) {
$tooltip.tooltip();
} else {
- $tooltip.tooltip('destroy');
+ $tooltip.tooltip('dispose');
}
});
},
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index e210d69895e..7144f4190e7 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -113,6 +113,8 @@ class List {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
+ issue.path = data.real_path;
+ issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index e177a3bfdc7..47efb3a8cee 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -141,6 +141,11 @@ export default class VariableList {
$rowClone.find(entry.selector).val(entry.default);
});
+ // Close any dropdowns
+ $rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
+ $dropdown.classList.remove('show');
+ });
+
this.initRow($rowClone);
$row.after($rowClone);
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
index 2e3ad244375..1e5c733d151 100644
--- a/app/assets/javascripts/clusters/clusters_index.js
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -1,20 +1,24 @@
-import Flash from '../flash';
-import { s__ } from '../locale';
-import setupToggleButtons from '../toggle_buttons';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import setupToggleButtons from '~/toggle_buttons';
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
import ClustersService from './services/clusters_service';
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
+
+ gcpSignupOffer();
+
// 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) => {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
- throw err;
- }),
+ 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/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index c2a35341eb2..fae580c091b 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -179,7 +179,7 @@
role="row"
>
<div
- class="alert alert-danger alert-block append-bottom-0 table-section section-100"
+ class="alert alert-danger alert-block append-bottom-0"
role="gridcell"
>
<div>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 9c12b89240c..bb5fcea648d 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -191,11 +191,11 @@ export default {
:value="ingressExternalIp"
readonly
/>
- <span class="input-group-btn">
+ <span class="input-group-append">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
- class="js-clipboard-btn"
+ class="input-group-text js-clipboard-btn"
/>
</span>
</div>
diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
new file mode 100644
index 00000000000..8bc20a1c09f
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/gcp_signup_offer.js
@@ -0,0 +1,27 @@
+import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+
+export default function gcpSignupOffer() {
+ const alertEl = document.querySelector('.gcp-signup-offer');
+ if (!alertEl) {
+ return;
+ }
+
+ const closeButtonEl = alertEl.getElementsByClassName('close')[0];
+ const { dismissEndpoint, featureId } = closeButtonEl.dataset;
+
+ closeButtonEl.addEventListener('click', () => {
+ axios
+ .post(dismissEndpoint, {
+ feature_name: featureId,
+ })
+ .then(() => {
+ $(alertEl).alert('close');
+ })
+ .catch(() => {
+ Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ });
+ });
+}
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index db96da4ccba..50e2949ab55 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -1,15 +1,7 @@
import $ from 'jquery';
// bootstrap jQuery plugins
-import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
-import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
-import 'bootstrap-sass/assets/javascripts/bootstrap/button';
-import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
-import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
-import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
-import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
-import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
-import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
+import 'bootstrap';
// custom jQuery functions
$.fn.extend({
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
deleted file mode 100644
index 303a5bf4a53..00000000000
--- a/app/assets/javascripts/compare.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
-
-import $ from 'jquery';
-import { localTimeAgo } from './lib/utils/datetime_utility';
-import axios from './lib/utils/axios_utils';
-
-export default class Compare {
- constructor(opts) {
- this.opts = opts;
- this.source_loading = $(".js-source-loading");
- this.target_loading = $(".js-target-loading");
- $('.js-compare-dropdown').each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- selectable: true,
- fieldName: $dropdown.data('fieldName'),
- filterable: true,
- id: function(obj, $el) {
- return $el.data('id');
- },
- toggleLabel: function(obj, $el) {
- return $el.text().trim();
- },
- clicked: function(e, el) {
- if ($dropdown.is('.js-target-branch')) {
- return _this.getTargetHtml();
- } else if ($dropdown.is('.js-source-branch')) {
- return _this.getSourceHtml();
- } else if ($dropdown.is('.js-target-project')) {
- return _this.getTargetProject();
- }
- }
- });
- };
- })(this));
- this.initialState();
- }
-
- initialState() {
- this.getSourceHtml();
- this.getTargetHtml();
- }
-
- getTargetProject() {
- $('.mr_target_commit').empty();
-
- return axios.get(this.opts.targetProjectUrl, {
- params: {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- },
- }).then(({ data }) => {
- $('.js-target-branch-dropdown .dropdown-content').html(data);
- });
- }
-
- getSourceHtml() {
- return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
- ref: $("input[name='merge_request[source_branch]']").val()
- });
- }
-
- getTargetHtml() {
- return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
- target_project_id: $("input[name='merge_request[target_project_id]']").val(),
- ref: $("input[name='merge_request[target_branch]']").val()
- });
- }
-
- static sendAjax(url, loading, target, params) {
- const $target = $(target);
-
- loading.show();
- $target.empty();
-
- return axios.get(url, {
- params,
- }).then(({ data }) => {
- loading.hide();
- $target.html(data);
- const className = '.' + $target[0].className.replace(' ', '.');
- localTimeAgo($('.js-timeago', className));
- });
- }
-}
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 260c91cac24..ffe15f02f2e 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+import { capitalizeFirstCharacter } from './lib/utils/text_utility';
-export default function initCompareAutocomplete() {
+export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({
data: function(term, callback) {
- axios.get($dropdown.data('refsUrl'), {
- params: {
- ref: $dropdown.data('ref'),
- search: term,
- },
- }).then(({ data }) => {
- callback(data);
- }).catch(() => flash(__('Error fetching refs')));
+ const params = {
+ ref: $dropdown.data('ref'),
+ search: term,
+ };
+
+ if (limitTo) {
+ params.find = limitTo;
+ }
+
+ axios
+ .get($dropdown.data('refsUrl'), {
+ params,
+ })
+ .then(({ data }) => {
+ if (limitTo) {
+ callback(data[capitalizeFirstCharacter(limitTo)] || []);
+ } else {
+ callback(data);
+ }
+ })
+ .catch(() => flash(__('Error fetching refs')));
},
selectable: true,
filterable: true,
@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) {
var link;
if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
+ return $('<li />')
+ .addClass('dropdown-header')
+ .text(ref.header);
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ link = $('<a />')
+ .attr('href', '#')
+ .addClass(ref === selected ? 'is-active' : '')
+ .text(ref)
+ .attr('data-ref', ref);
return $('<li />').append(link);
}
},
@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
- }
+ },
+ clicked: () => clickHandler($dropdown),
});
- $filterInput.on('keyup', (e) => {
+ $filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return;
const text = $filterInput.val();
@@ -54,10 +75,10 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open');
});
- $dropdownContainer.on('click', '.dropdown-content a', (e) => {
+ $dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) {
- $dropdown.tooltip('fixTitle');
+ $dropdown.tooltip('_fixTitle');
}
});
});
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index fb1fc9cd32e..09d490106df 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -61,8 +61,8 @@ export default class CreateMergeRequestDropdown {
}
available() {
- this.availableButton.classList.remove('hide');
- this.unavailableButton.classList.add('hide');
+ this.availableButton.classList.remove('hidden');
+ this.unavailableButton.classList.add('hidden');
}
bindEvents() {
@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
+ this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
- } else if (data.has_related_branch) {
+ } else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
- Flash('Failed to check if a new branch can be created.');
+ Flash(__('Failed to check related branches.'));
});
}
@@ -231,7 +232,7 @@ export default class CreateMergeRequestDropdown {
}
hide() {
- this.wrapperEl.classList.add('hide');
+ this.wrapperEl.classList.add('hidden');
}
init() {
@@ -405,8 +406,13 @@ export default class CreateMergeRequestDropdown {
}
unavailable() {
- this.availableButton.classList.add('hide');
- this.unavailableButton.classList.remove('hide');
+ this.availableButton.classList.add('hidden');
+ this.unavailableButton.classList.remove('hidden');
+ }
+
+ updateBranchName(suggestedBranchName) {
+ this.branchInput.value = suggestedBranchName;
+ this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) {
@@ -414,8 +420,6 @@ export default class CreateMergeRequestDropdown {
// ref - string - what a user typed.
// result - string - what has been found on backend.
- const pathReplacement = `$1${ref}`;
-
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
- this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
- pathReplacement);
- this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
- pathReplacement);
+ this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
+
+ // target - 'branch' or 'ref'
+ // ref - string - the new value to use as branch or ref
+ updateCreatePaths(target, ref) {
+ const pathReplacement = `$1${ref}`;
+
+ this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
+ pathReplacement);
+ this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
+ pathReplacement);
+ }
}
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
index 32ae0cc1476..5be17081b58 100644
--- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue
@@ -16,7 +16,7 @@
<template>
<span
v-if="count === 50"
- class="events-info pull-right"
+ class="events-info float-right"
>
<i
class="fa fa-warning"
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 87f8854f940..1c43fc3cdc7 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -82,7 +82,6 @@ export default () => {
this.service
.fetchCycleAnalyticsData(fetchOptions)
- .then(resp => resp.json())
.then((response) => {
this.store.setCycleAnalyticsData(response);
this.selectDefaultStage();
@@ -116,7 +115,6 @@ export default () => {
stage,
startDate: this.startDate,
})
- .then(resp => resp.json())
.then((response) => {
this.isEmptyStage = !response.events.length;
this.store.setStageEvents(response.events, stage);
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
index f496c38208d..4cf416c50e5 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
@@ -1,16 +1,20 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class CycleAnalyticsService {
constructor(options) {
- this.requestPath = options.requestPath;
- this.cycleAnalytics = Vue.resource(this.requestPath);
+ this.axios = axios.create({
+ baseURL: options.requestPath,
+ });
}
fetchCycleAnalyticsData(options = { startDate: 30 }) {
- return this.cycleAnalytics.get({ cycle_analytics: { start_date: options.startDate } });
+ return this.axios
+ .get('', {
+ params: {
+ 'cycle_analytics[start_date]': options.startDate,
+ },
+ })
+ .then(x => x.data);
}
fetchStageData(options) {
@@ -19,12 +23,12 @@ export default class CycleAnalyticsService {
startDate,
} = options;
- return Vue.http.get(`${this.requestPath}/events/${stage.name}.json`, {
- params: {
- cycle_analytics: {
- start_date: startDate,
+ return this.axios
+ .get(`events/${stage.name}.json`, {
+ params: {
+ 'cycle_analytics[start_date]': startDate,
},
- },
- });
+ })
+ .then(x => x.data);
}
}
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index b839b9f286f..67dda0e29cb 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,55 +1,50 @@
<script>
- import eventHub from '../eventhub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+import eventHub from '../eventhub';
- export default {
- components: {
- loadingIcon,
+export default {
+ components: {
+ loadingIcon,
+ },
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
},
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- btnCssClass: {
- type: String,
- required: false,
- default: 'btn-default',
- },
+ type: {
+ type: String,
+ required: true,
},
- data() {
- return {
- isLoading: false,
- };
+ btnCssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
},
- computed: {
- text() {
- return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
- },
- },
- methods: {
- doAction() {
- this.isLoading = true;
+ },
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ methods: {
+ doAction() {
+ this.isLoading = true;
- eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
- this.isLoading = false;
- });
- },
+ eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
+ this.isLoading = false;
+ });
},
- };
+ },
+};
</script>
<template>
<button
- class="btn btn-sm prepend-left-10"
+ class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
- {{ text }}
+ <slot></slot>
<loading-icon
v-if="isLoading"
:inline="true"
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 5a782237b7d..c41fe55db63 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,80 +1,115 @@
<script>
- import Flash from '../../flash';
- import eventHub from '../eventhub';
- import DeployKeysService from '../service';
- import DeployKeysStore from '../store';
- import keysPanel from './keys_panel.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import { s__ } from '~/locale';
+import Flash from '~/flash';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import eventHub from '../eventhub';
+import DeployKeysService from '../service';
+import DeployKeysStore from '../store';
+import KeysPanel from './keys_panel.vue';
- export default {
- components: {
- keysPanel,
- loadingIcon,
+export default {
+ components: {
+ KeysPanel,
+ LoadingIcon,
+ NavigationTabs,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- props: {
- endpoint: {
- type: String,
- required: true,
- },
+ projectId: {
+ type: String,
+ required: true,
},
- data() {
- return {
- isLoading: false,
- store: new DeployKeysStore(),
- };
+ },
+ data() {
+ return {
+ currentTab: 'enabled_keys',
+ isLoading: false,
+ store: new DeployKeysStore(),
+ };
+ },
+ scopes: {
+ enabled_keys: s__('DeployKeys|Enabled deploy keys'),
+ available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
+ public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
+ },
+ computed: {
+ tabs() {
+ return Object.keys(this.$options.scopes).map(scope => {
+ const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
+
+ return {
+ name: this.$options.scopes[scope],
+ scope,
+ isActive: scope === this.currentTab,
+ count,
+ };
+ });
+ },
+ hasKeys() {
+ return Object.keys(this.keys).length;
},
- computed: {
- hasKeys() {
- return Object.keys(this.keys).length;
- },
- keys() {
- return this.store.keys;
- },
+ keys() {
+ return this.store.keys;
},
- created() {
- this.service = new DeployKeysService(this.endpoint);
+ },
+ created() {
+ this.service = new DeployKeysService(this.endpoint);
- eventHub.$on('enable.key', this.enableKey);
- eventHub.$on('remove.key', this.disableKey);
- eventHub.$on('disable.key', this.disableKey);
+ eventHub.$on('enable.key', this.enableKey);
+ eventHub.$on('remove.key', this.disableKey);
+ eventHub.$on('disable.key', this.disableKey);
+ },
+ mounted() {
+ this.fetchKeys();
+ },
+ beforeDestroy() {
+ eventHub.$off('enable.key', this.enableKey);
+ eventHub.$off('remove.key', this.disableKey);
+ eventHub.$off('disable.key', this.disableKey);
+ },
+ methods: {
+ onChangeTab(tab) {
+ this.currentTab = tab;
},
- mounted() {
- this.fetchKeys();
+ fetchKeys() {
+ this.isLoading = true;
+
+ return this.service
+ .getKeys()
+ .then(data => {
+ this.isLoading = false;
+ this.store.keys = data;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ this.store.keys = {};
+ return new Flash(s__('DeployKeys|Error getting deploy keys'));
+ });
},
- beforeDestroy() {
- eventHub.$off('enable.key', this.enableKey);
- eventHub.$off('remove.key', this.disableKey);
- eventHub.$off('disable.key', this.disableKey);
+ enableKey(deployKey) {
+ this.service
+ .enableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
- methods: {
- fetchKeys() {
- this.isLoading = true;
-
- this.service.getKeys()
- .then((data) => {
- this.isLoading = false;
- this.store.keys = data;
- })
- .catch(() => new Flash('Error getting deploy keys'));
- },
- enableKey(deployKey) {
- this.service.enableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .catch(() => new Flash('Error enabling deploy key'));
- },
- disableKey(deployKey, callback) {
- // eslint-disable-next-line no-alert
- if (confirm('You are going to remove this deploy key. Are you sure?')) {
- this.service.disableKey(deployKey.id)
- .then(() => this.fetchKeys())
- .then(callback)
- .catch(() => new Flash('Error removing deploy key'));
- } else {
- callback();
- }
- },
+ disableKey(deployKey, callback) {
+ // eslint-disable-next-line no-alert
+ if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ this.service
+ .disableKey(deployKey.id)
+ .then(this.fetchKeys)
+ .then(callback)
+ .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
+ } else {
+ callback();
+ }
},
- };
+ },
+};
</script>
<template>
@@ -82,29 +117,38 @@
<loading-icon
v-if="isLoading && !hasKeys"
size="2"
- label="Loading deploy keys"
+ :label="s__('DeployKeys|Loading deploy keys')"
/>
- <div v-else-if="hasKeys">
+ <template v-else-if="hasKeys">
+ <div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
+ <div class="fade-left">
+ <i
+ class="fa fa-angle-left"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+ <div class="fade-right">
+ <i
+ class="fa fa-angle-right"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+
+ <navigation-tabs
+ :tabs="tabs"
+ @onChangeTab="onChangeTab"
+ scope="deployKeys"
+ />
+ </div>
<keys-panel
- title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
- :keys="keys.enabled_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- title="Deploy keys from projects you have access to"
- :keys="keys.available_project_keys"
- :store="store"
- :endpoint="endpoint"
- />
- <keys-panel
- v-if="keys.public_keys.length"
- title="Public deploy keys available to any project"
- :keys="keys.public_keys"
+ :project-id="projectId"
+ :keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
/>
- </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index c6091efd62f..6c2af7fa768 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -1,111 +1,235 @@
<script>
- import actionBtn from './action_btn.vue';
- import { getTimeago } from '../../lib/utils/datetime_utility';
- import tooltip from '../../vue_shared/directives/tooltip';
+import _ from 'underscore';
+import { s__, sprintf } from '~/locale';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
- export default {
- components: {
- actionBtn,
- },
- directives: {
- tooltip,
- },
- props: {
- deployKey: {
- type: Object,
- required: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
- },
- computed: {
- timeagoDate() {
- return getTimeago().format(this.deployKey.created_at);
- },
- editDeployKeyPath() {
- return `${this.endpoint}/${this.deployKey.id}/edit`;
- },
- },
- methods: {
- isEnabled(id) {
- return this.store.findEnabledKey(id) !== undefined;
- },
- tooltipTitle(project) {
- return project.can_push ? 'Write access allowed' : 'Read access only';
- },
- },
- };
+import actionBtn from './action_btn.vue';
+
+export default {
+ components: {
+ actionBtn,
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ projectsExpanded: false,
+ };
+ },
+ computed: {
+ editDeployKeyPath() {
+ return `${this.endpoint}/${this.deployKey.id}/edit`;
+ },
+ projects() {
+ const projects = [...this.deployKey.deploy_keys_projects];
+
+ if (this.projectId !== null) {
+ const indexOfCurrentProject = _.findIndex(
+ projects,
+ project =>
+ project &&
+ project.project &&
+ project.project.id &&
+ project.project.id.toString() === this.projectId,
+ );
+
+ if (indexOfCurrentProject > -1) {
+ const currentProject = projects.splice(indexOfCurrentProject, 1);
+ currentProject[0].project.full_name = s__('DeployKeys|Current project');
+ return currentProject.concat(projects);
+ }
+ }
+ return projects;
+ },
+ firstProject() {
+ return _.head(this.projects);
+ },
+ restProjects() {
+ return _.tail(this.projects);
+ },
+ restProjectsTooltip() {
+ return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
+ count: this.restProjects.length,
+ });
+ },
+ restProjectsLabel() {
+ return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
+ },
+ isEnabled() {
+ return this.store.isEnabled(this.deployKey.id);
+ },
+ isRemovable() {
+ return (
+ this.store.isEnabled(this.deployKey.id) &&
+ this.deployKey.destroyed_when_orphaned &&
+ this.deployKey.almost_orphaned
+ );
+ },
+ isExpandable() {
+ return !this.projectsExpanded && this.restProjects.length > 1;
+ },
+ isExpanded() {
+ return this.projectsExpanded || this.restProjects.length === 1;
+ },
+ },
+ methods: {
+ projectTooltipTitle(project) {
+ return project.can_push
+ ? s__('DeployKeys|Write access allowed')
+ : s__('DeployKeys|Read access only');
+ },
+ toggleExpanded() {
+ this.projectsExpanded = !this.projectsExpanded;
+ },
+ },
+};
</script>
<template>
- <div>
- <div class="pull-left append-right-10 hidden-xs">
- <i
- aria-hidden="true"
- class="fa fa-key key-icon"
- >
- </i>
+ <div class="gl-responsive-table-row deploy-key">
+ <div class="table-section section-40">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div class="table-mobile-content">
+ <strong class="title qa-key-title">
+ {{ deployKey.title }}
+ </strong>
+ <div class="fingerprint qa-key-fingerprint">
+ {{ deployKey.fingerprint }}
+ </div>
+ </div>
</div>
- <div class="deploy-key-content key-list-item-info">
- <strong class="title qa-key-title">
- {{ deployKey.title }}
- </strong>
- <div class="description qa-key-fingerprint">
- {{ deployKey.fingerprint }}
+ <div class="table-section section-30 section-wrap">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div class="table-mobile-content deploy-project-list">
+ <template v-if="projects.length > 0">
+ <a
+ class="label deploy-project-label"
+ :title="projectTooltipTitle(firstProject)"
+ v-tooltip
+ >
+ <span>
+ {{ firstProject.project.full_name }}
+ </span>
+ <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ <a
+ v-if="isExpandable"
+ class="label deploy-project-label"
+ @click="toggleExpanded"
+ :title="restProjectsTooltip"
+ v-tooltip
+ >
+ <span>{{ restProjectsLabel }}</span>
+ </a>
+ <a
+ v-else-if="isExpanded"
+ v-for="deployKeysProject in restProjects"
+ :key="deployKeysProject.project.full_path"
+ class="label deploy-project-label"
+ :href="deployKeysProject.project.full_path"
+ :title="projectTooltipTitle(deployKeysProject)"
+ v-tooltip
+ >
+ <span>
+ {{ deployKeysProject.project.full_name }}
+ </span>
+ <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
+ </a>
+ </template>
+ <span
+ v-else
+ class="text-secondary">{{ __('None') }}</span>
</div>
</div>
- <div class="deploy-key-content prepend-left-default deploy-key-projects">
- <a
- v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
- :key="i"
- class="label deploy-project-label"
- :href="deployKeysProject.project.full_path"
- :title="tooltipTitle(deployKeysProject)"
- v-tooltip
- >
- {{ deployKeysProject.project.full_name }}
- <i
- v-if="!deployKeysProject.can_push"
- aria-hidden="true"
- class="fa fa-lock"
- >
- </i>
- </a>
+ <div class="table-section section-15 text-right">
+ <div
+ role="rowheader"
+ class="table-mobile-header">
+ {{ __('Created') }}
+ </div>
+ <div class="table-mobile-content text-secondary key-created-at">
+ <span
+ :title="tooltipTitle(deployKey.created_at)"
+ v-tooltip>
+ <icon name="calendar"/>
+ <span>{{ timeFormated(deployKey.created_at) }}</span>
+ </span>
+ </div>
</div>
- <div class="deploy-key-content">
- <span class="key-created-at">
- created {{ timeagoDate }}
- </span>
- <a
- v-if="deployKey.can_edit"
- class="btn btn-sm"
- :href="editDeployKeyPath"
- >
- Edit
- </a>
- <action-btn
- v-if="!isEnabled(deployKey.id)"
- :deploy-key="deployKey"
- type="enable"
- />
- <action-btn
- v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="remove"
- />
- <action-btn
- v-else
- :deploy-key="deployKey"
- btn-css-class="btn-warning"
- type="disable"
- />
+ <div class="table-section section-15 table-button-footer deploy-key-actions">
+ <div class="btn-group table-action-buttons">
+ <action-btn
+ v-if="!isEnabled"
+ :deploy-key="deployKey"
+ type="enable"
+ >
+ {{ __('Enable') }}
+ </action-btn>
+ <a
+ v-if="deployKey.can_edit"
+ class="btn btn-default text-secondary"
+ :href="editDeployKeyPath"
+ :title="__('Edit')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="pencil"/>
+ </a>
+ <action-btn
+ v-if="isRemovable"
+ :deploy-key="deployKey"
+ btn-css-class="btn-danger"
+ type="remove"
+ :title="__('Remove')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="remove"/>
+ </action-btn>
+ <action-btn
+ v-else-if="isEnabled"
+ :deploy-key="deployKey"
+ btn-css-class="btn-warning"
+ type="disable"
+ :title="__('Disable')"
+ data-container="body"
+ v-tooltip
+ >
+ <icon name="cancel"/>
+ </action-btn>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index 822b0323156..3b146c7389a 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -1,62 +1,68 @@
<script>
- import key from './key.vue';
+import deployKey from './key.vue';
- export default {
- components: {
- key,
+export default {
+ components: {
+ deployKey,
+ },
+ props: {
+ keys: {
+ type: Array,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- keys: {
- type: Array,
- required: true,
- },
- showHelpBox: {
- type: Boolean,
- required: false,
- default: true,
- },
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
+ store: {
+ type: Object,
+ required: true,
},
- };
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
</script>
<template>
- <div class="deploy-keys-panel">
- <h5>
- {{ title }}
- ({{ keys.length }})
- </h5>
- <ul
- class="well-list"
- v-if="keys.length"
- >
- <li
+ <div class="deploy-keys-panel table-holder">
+ <template v-if="keys.length > 0">
+ <div
+ role="row"
+ class="gl-responsive-table-row table-row-header">
+ <div
+ role="rowheader"
+ class="table-section section-40">
+ {{ s__('DeployKeys|Deploy key') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-30">
+ {{ s__('DeployKeys|Project usage') }}
+ </div>
+ <div
+ role="rowheader"
+ class="table-section section-15 text-right">
+ {{ __('Created') }}
+ </div>
+ </div>
+ <deploy-key
v-for="deployKey in keys"
:key="deployKey.id"
- >
- <key
- :deploy-key="deployKey"
- :store="store"
- :endpoint="endpoint"
- />
- </li>
- </ul>
+ :deploy-key="deployKey"
+ :store="store"
+ :endpoint="endpoint"
+ :project-id="projectId"
+ />
+ </template>
<div
class="settings-message text-center"
- v-else-if="showHelpBox"
+ v-else
>
- No deploy keys found. Create one with the form above.
+ {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index b727261648c..6e439be42ae 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -1,21 +1,24 @@
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
-export default () => new Vue({
- el: document.getElementById('js-deploy-keys'),
- components: {
- deployKeysApp,
- },
- data() {
- return {
- endpoint: this.$options.el.dataset.endpoint,
- };
- },
- render(createElement) {
- return createElement('deploy-keys-app', {
- props: {
- endpoint: this.endpoint,
- },
- });
- },
-});
+export default () =>
+ new Vue({
+ el: document.getElementById('js-deploy-keys'),
+ components: {
+ deployKeysApp,
+ },
+ data() {
+ return {
+ endpoint: this.$options.el.dataset.endpoint,
+ projectId: this.$options.el.dataset.projectId,
+ };
+ },
+ render(createElement) {
+ return createElement('deploy-keys-app', {
+ props: {
+ endpoint: this.endpoint,
+ projectId: this.projectId,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
index fe6dbaa9498..9dc3b21f6f6 100644
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -1,34 +1,24 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class DeployKeysService {
constructor(endpoint) {
- this.endpoint = endpoint;
-
- this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
- enable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/enable`,
- },
- disable: {
- method: 'PUT',
- url: `${this.endpoint}{/id}/disable`,
- },
+ this.axios = axios.create({
+ baseURL: endpoint,
});
}
getKeys() {
- return this.resource.get()
- .then(response => response.json());
+ return this.axios.get()
+ .then(response => response.data);
}
enableKey(id) {
- return this.resource.enable({ id }, {});
+ return this.axios.put(`${id}/enable`)
+ .then(response => response.data);
}
disableKey(id) {
- return this.resource.disable({ id }, {});
+ return this.axios.put(`${id}/disable`)
+ .then(response => response.data);
}
}
diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js
index 6210361af26..a350bc99a70 100644
--- a/app/assets/javascripts/deploy_keys/store/index.js
+++ b/app/assets/javascripts/deploy_keys/store/index.js
@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {};
}
- findEnabledKey(id) {
- return this.keys.enabled_keys.find(key => key.id === id);
+ isEnabled(id) {
+ return this.keys.enabled_keys.some(key => key.id === id);
}
}
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 180a6bd67e7..fe9b0795609 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -79,7 +79,7 @@ const DiffNoteAvatars = Vue.extend({
storeState: {
handler() {
this.$nextTick(() => {
- $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('_fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
@@ -138,7 +138,7 @@ const DiffNoteAvatars = Vue.extend({
this.$nextTick(() => {
this.setDiscussionVisible();
- $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('_fixTitle');
$('.has-tooltip', this.$el).tooltip('hide');
});
},
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index df4c72ba0ed..8d66417abac 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -61,7 +61,7 @@ const ResolveBtn = Vue.extend({
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
});
},
resolve: function () {
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 842a4255f08..4164149dd06 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
+import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
+ this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
+ const hasDueDate = this.displayedDate !== 'No due date';
+ const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
+ const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+
return this.$loading.fadeOut();
});
}
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index dc7672560ea..cd8dff40b88 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [],
flags: [],
};
- Object.keys(emojiMap).forEach((name) => {
+ Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
- const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
+ const fallbackSpriteAttribute = opts.sprite
+ ? `data-fallback-sprite-class="${fallbackSpriteClass}"`
+ : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index c18d07dad43..8c1861c56db 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
- const hasColor = imageDataArray[indexOffset + 0] ||
+ const hasColor =
+ imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16;
function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
- const numTestEntries = testMapKeys
- .reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
+ const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
+ .length;
const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d');
- canvas.width = (2 * fontSize);
- canvas.height = (numTestEntries * fontSize);
+ canvas.width = 2 * fontSize;
+ canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
- [].concat(testEntry).forEach((emojiUnicode) => {
- ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
+ [].concat(testEntry).forEach(emojiUnicode => {
+ ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1;
});
});
@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas
const resultMap = {};
let readIndex = 0;
- testMapKeys.forEach((testKey) => {
+ testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries
- const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => {
+ const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters
- const imageData = ctx.getImageData(
- 0,
- (readIndex * fontSize) + (fontSize / 2),
- 2 * fontSize,
- 1,
- ).data;
+ const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
+ .data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
- const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
+ const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
- // Check to see that nothing is rendered next to the first character
- // to ensure that the ZWJ sequence rendered as one piece
+ // Check to see that nothing is rendered next to the first character
+ // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
- window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
+ window.localStorage.setItem(
+ 'gl-emoji-unicode-support-map',
+ JSON.stringify(unicodeSupportMap),
+ );
}
}
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index dbee81fa320..6bd7c6b49cb 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -43,6 +43,7 @@
<div class="environments-container">
<loading-icon
+ class="prepend-top-default"
label="Loading environments"
v-if="isLoading"
size="3"
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 16bd2f5feb3..0b3fef9fcca 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,5 +1,5 @@
<script>
- import playIconSvg from 'icons/_icon_play.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -8,9 +8,9 @@
directives: {
tooltip,
},
-
components: {
loadingIcon,
+ Icon,
},
props: {
actions: {
@@ -19,20 +19,16 @@
default: () => [],
},
},
-
data() {
return {
- playIconSvg,
isLoading: false,
};
},
-
computed: {
title() {
return 'Deploy to...';
},
},
-
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@@ -65,7 +61,10 @@
:disabled="isLoading"
>
<span>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<i
class="fa fa-caret-down"
aria-hidden="true"
@@ -75,7 +74,7 @@
</span>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(action, i) in actions"
:key="i">
@@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)"
>
- <span v-html="playIconSvg"></span>
+ <icon
+ name="play"
+ :size="12"
+ />
<span>
{{ action.name }}
</span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index c9a68cface6..ea6f1168c68 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,4 +1,5 @@
<script>
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
@@ -6,6 +7,9 @@
* Renders the external url link in environments table.
*/
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
@@ -15,7 +19,6 @@
required: true,
},
},
-
computed: {
title() {
return s__('Environments|Open');
@@ -34,10 +37,9 @@
:aria-label="title"
:href="externalUrl"
>
- <i
- class="fa fa-external-link"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="external-link"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 79326ca3487..23aaab2c441 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -486,14 +486,14 @@
{{ model.folderName }}
</span>
- <span class="badge">
+ <span class="badge badge-pill">
{{ model.size }}
</span>
</span>
</div>
<div
- class="table-section section-10 deployment-column hidden-xs hidden-sm"
+ class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
role="gridcell"
>
<span v-if="shouldRenderDeploymentID">
@@ -513,7 +513,7 @@
</div>
<div
- class="table-section section-15 hidden-xs hidden-sm"
+ class="table-section section-15 d-none d-sm-none d-md-block"
role="gridcell"
>
<a
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 081537cf218..8df1b6317e3 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -2,20 +2,22 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
monitoringUrl: {
type: String,
required: true,
},
},
-
computed: {
title() {
return 'Monitoring';
@@ -26,17 +28,16 @@
<template>
<a
v-tooltip
- class="btn monitoring-url hidden-xs hidden-sm"
+ class="btn monitoring-url d-none d-sm-none d-md-block"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title"
>
- <i
- class="fa fa-area-chart"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="chart"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 605a88e997e..7515d711c50 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -12,7 +12,6 @@
components: {
loadingIcon,
},
-
props: {
retryUrl: {
type: String,
@@ -24,13 +23,11 @@
default: true,
},
},
-
data() {
return {
isLoading: false,
};
},
-
methods: {
onClick() {
this.isLoading = true;
@@ -43,7 +40,7 @@
<template>
<button
type="button"
- class="btn hidden-xs hidden-sm"
+ class="btn d-none d-sm-none d-md-block"
@click="onClick"
:disabled="isLoading"
>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index dda7429a726..7055f208451 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -43,7 +43,7 @@
if (confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
- $(this.$el).tooltip('destroy');
+ $(this.$el).tooltip('dispose');
eventHub.$emit('postAction', this.stopUrl);
}
@@ -55,7 +55,7 @@
<button
v-tooltip
type="button"
- class="btn stop-env-link hidden-xs hidden-sm"
+ class="btn stop-env-link d-none d-sm-none d-md-block"
data-container="body"
@click="onClick"
:disabled="isLoading"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index 407d5333c0e..0dbbbb75e07 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
- import terminalIconSvg from 'icons/_icon_terminal.svg';
+ import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
-
props: {
terminalPath: {
type: String,
@@ -18,13 +20,6 @@
default: '',
},
},
-
- data() {
- return {
- terminalIconSvg,
- };
- },
-
computed: {
title() {
return 'Terminal';
@@ -35,12 +30,15 @@
<template>
<a
v-tooltip
- class="btn terminal-button hidden-xs hidden-sm"
+ class="btn terminal-button d-none d-sm-none d-md-block"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath"
- v-html="terminalIconSvg"
>
+ <icon
+ name="terminal"
+ :size="12"
+ />
</a>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index c0be72f7401..3da762446c9 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -68,8 +68,7 @@
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folder.folder_path)
- .then(resp => resp.json())
- .then(response => this.store.setfolderContent(folder, response.environments))
+ .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.'));
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 34d18d55120..a7a79dbca70 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -6,7 +6,6 @@ import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import {
getParameterByName,
- parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import Flash from '../../flash';
@@ -46,17 +45,14 @@ export default {
methods: {
saveData(resp) {
- const headers = resp.headers;
- return resp.json().then((response) => {
- this.isLoading = false;
-
- if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
- this.store.storeAvailableCount(response.available_count);
- this.store.storeStoppedCount(response.stopped_count);
- this.store.storeEnvironments(response.environments);
- this.store.setPagination(headers);
- }
- });
+ this.isLoading = false;
+
+ if (_.isEqual(resp.config.params, this.requestData)) {
+ this.store.storeAvailableCount(resp.data.available_count);
+ this.store.storeStoppedCount(resp.data.stopped_count);
+ this.store.storeEnvironments(resp.data.environments);
+ this.store.setPagination(resp.headers);
+ }
},
/**
@@ -70,7 +66,7 @@ export default {
updateContent(parameters) {
this.updateInternalState(parameters);
// fetch new data
- return this.service.get(this.requestData)
+ return this.service.fetchEnvironments(this.requestData)
.then(response => this.successCallback(response))
.then(() => {
// restart polling
@@ -105,7 +101,7 @@ export default {
fetchEnvironments() {
this.isLoading = true;
- return this.service.get(this.requestData)
+ return this.service.fetchEnvironments(this.requestData)
.then(this.successCallback)
.catch(this.errorCallback);
},
@@ -141,7 +137,7 @@ export default {
this.poll = new Poll({
resource: this.service,
- method: 'get',
+ method: 'fetchEnvironments',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 03ab74b3338..3b121551aca 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,25 +1,22 @@
-/* eslint-disable class-methods-use-this */
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-Vue.use(VueResource);
+import axios from '~/lib/utils/axios_utils';
export default class EnvironmentsService {
constructor(endpoint) {
- this.environments = Vue.resource(endpoint);
+ this.environmentsEndpoint = endpoint;
this.folderResults = 3;
}
- get(options = {}) {
+ fetchEnvironments(options = {}) {
const { scope, page } = options;
- return this.environments.get({ scope, page });
+ return axios.get(this.environmentsEndpoint, { params: { scope, page } });
}
+ // eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
- return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ return axios.post(endpoint, {}, { emulateJSON: true });
}
getFolderContent(folderUrl) {
- return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
+ return axios.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
index 2d5bae9a9c4..2f27c9351bc 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -24,7 +24,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
- <div class="popover-content"></div>
+ <div class="popover-body"></div>
</div>
`,
})
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 7e9770a9ea2..9de57db48fd 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -408,7 +408,10 @@ class GfmAutoComplete {
fetchData($input, at) {
if (this.isLoadingData[at]) return;
+
this.isLoadingData[at] = true;
+ const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
+
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
@@ -418,12 +421,14 @@ class GfmAutoComplete {
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
- } else {
- AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
+ } else if (dataSource) {
+ AjaxCache.retrieve(dataSource, true)
.then((data) => {
this.loadData($input, at, data);
})
.catch(() => { this.isLoadingData[at] = false; });
+ } else {
+ this.isLoadingData[at] = false;
}
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index fa48d7d1915..746a06b7c4f 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -374,7 +374,7 @@ GitLabDropdown = (function() {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
- return _this.dropdown.removeClass('open');
+ return _this.dropdown.removeClass('show');
}
}
};
@@ -801,7 +801,7 @@ GitLabDropdown = (function() {
if (this.options.filterable) {
const initialScrollTop = $(window).scrollTop();
- if (this.dropdown.is('.open')) {
+ if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
this.filterInput.focus();
}
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index 972b2252acb..87c6e37b9fb 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -62,7 +62,7 @@ export default class GlFieldError {
this.inputDomElement = this.inputElement.get(0);
this.form = formErrors;
this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
- this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`);
+ this.fieldErrorElement = $(`<p class='${errorMessageClass} hidden'>${this.errorMessage}</p>`);
this.state = {
valid: false,
@@ -146,8 +146,8 @@ export default class GlFieldError {
renderInvalid() {
this.inputElement.addClass(inputErrorClass);
- this.scopedSiblings.hide();
- return this.fieldErrorElement.show();
+ this.scopedSiblings.addClass('hidden');
+ return this.fieldErrorElement.removeClass('hidden');
}
renderClear() {
@@ -157,7 +157,7 @@ export default class GlFieldError {
this.accessCurrentValue(trimmedInput);
}
this.inputElement.removeClass(inputErrorClass);
- this.scopedSiblings.hide();
- this.fieldErrorElement.hide();
+ this.scopedSiblings.addClass('hidden');
+ this.fieldErrorElement.addClass('hidden');
}
}
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 502e3569321..029fd6a67d4 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -7,12 +7,12 @@ import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
- const form = $('.commits-search-form');
+ const tag = $('.js-signature-container');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
- const params = parseQueryStringIntoObject(form.serialize());
- return axios.get(form.data('signaturesPath'), { params })
+ const params = parseQueryStringIntoObject(tag.serialize());
+ return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 764b130fdb8..7f64a9bd741 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -99,7 +99,7 @@ export default {
/>
</div>
<div
- class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs"
+ class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block"
:class="{ 'content-loading': group.isChildrenLoading }"
>
<a
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
new file mode 100644
index 00000000000..05dbc1410de
--- /dev/null
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -0,0 +1,106 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { activityBarViews } from '../constants';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters(['currentProject', 'hasChanges']),
+ ...mapState(['currentActivityView']),
+ goBackUrl() {
+ return document.referrer || this.currentProject.web_url;
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <nav class="ide-activity-bar">
+ <ul class="list-unstyled">
+ <li v-once>
+ <a
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ :href="goBackUrl"
+ class="ide-sidebar-link"
+ :title="s__('IDE|Go back')"
+ :aria-label="s__('IDE|Go back')"
+ >
+ <icon
+ :size="16"
+ name="go-back"
+ />
+ </a>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-edit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.edit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.edit)"
+ :title="s__('IDE|Edit')"
+ :aria-label="s__('IDE|Edit')"
+ >
+ <icon
+ name="code"
+ />
+ </button>
+ </li>
+ <li>
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-review-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.review
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.review)"
+ :title="s__('IDE|Review')"
+ :aria-label="s__('IDE|Review')"
+ >
+ <icon
+ name="file-modified"
+ />
+ </button>
+ </li>
+ <li v-show="hasChanges">
+ <button
+ v-tooltip
+ data-container="body"
+ data-placement="right"
+ type="button"
+ class="ide-sidebar-link js-ide-commit-mode"
+ :class="{
+ active: currentActivityView === $options.activityBarViews.commit
+ }"
+ @click.prevent="updateActivityBarView($options.activityBarViews.commit)"
+ :title="s__('IDE|Commit')"
+ :aria-label="s__('IDE|Commit')"
+ >
+ <icon
+ name="commit"
+ />
+ </button>
+ </li>
+ </ul>
+ </nav>
+</template>
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 037e3efb4ce..1cec84706fc 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,31 +1,88 @@
<script>
-import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import { pluralize } from '~/lib/utils/text_utility';
+import { __, sprintf } from '~/locale';
export default {
components: {
- icon,
+ Icon,
+ },
+ directives: {
+ tooltip,
},
props: {
file: {
type: Object,
required: true,
},
+ showTooltip: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showStagedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ forceModifiedIcon: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
changedIcon() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
+ return this.file.tempFile && !this.forceModifiedIcon
+ ? `file-addition${suffix}`
+ : `file-modified${suffix}`;
+ },
+ stagedIcon() {
+ return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon}`;
+ return `multi-${this.changedIcon} pull-left`;
+ },
+ tooltipTitle() {
+ if (!this.showTooltip) return undefined;
+
+ const type = this.file.tempFile ? 'addition' : 'modification';
+
+ if (this.file.changed && !this.file.staged) {
+ return sprintf(__('Unstaged %{type}'), {
+ type,
+ });
+ } else if (!this.file.changed && this.file.staged) {
+ return sprintf(__('Staged %{type}'), {
+ type,
+ });
+ } else if (this.file.changed && this.file.staged) {
+ return sprintf(__('Unstaged and staged %{type}'), {
+ type: pluralize(type),
+ });
+ }
+
+ return undefined;
},
},
};
</script>
<template>
- <icon
- :name="changedIcon"
- :size="12"
- :css-classes="`ide-file-changed-icon ${changedIconClass}`"
- />
+ <span
+ v-tooltip
+ :title="tooltipTitle"
+ data-container="body"
+ data-placement="right"
+ class="ide-file-changed-icon"
+ >
+ <icon
+ v-if="file.changed || file.tempFile || file.staged"
+ :name="changedIcon"
+ :size="12"
+ :css-classes="changedIconClass"
+ />
+ </span>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 45321df191c..b4f3778d946 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
@@ -9,7 +9,8 @@ export default {
RadioGroup,
},
computed: {
- ...mapState(['currentBranchId']),
+ ...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
+ ...mapGetters(['currentProject']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
@@ -17,6 +18,17 @@ export default {
false,
);
},
+ disableMergeRequestRadio() {
+ return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
+ },
+ },
+ mounted() {
+ if (this.disableMergeRequestRadio) {
+ this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
+ }
+ },
+ methods: {
+ ...mapActions('commit', ['updateCommitAction']),
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
@@ -41,9 +53,11 @@ export default {
:show-input="true"
/>
<radio-group
+ v-if="currentProject.merge_requests_enabled"
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
+ :disabled="disableMergeRequestRadio"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
new file mode 100644
index 00000000000..d0a60d647e5
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue
@@ -0,0 +1,36 @@
+<script>
+import { mapState } from 'vuex';
+
+export default {
+ computed: {
+ ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="!lastCommitMsg"
+ class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
+ >
+ <div
+ class="ide-commit-empty-state-container"
+ >
+ <div class="svg-content svg-80">
+ <img :src="noChangesStateSvgPath" />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ >
+ <h4>
+ {{ __('No changes') }}
+ </h4>
+ <p>
+ {{ __('Edit files in the editor and commit changes here') }}
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
new file mode 100644
index 00000000000..81961fe3c57
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -0,0 +1,171 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { sprintf, __ } from '~/locale';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import CommitMessageField from './message_field.vue';
+import Actions from './actions.vue';
+import SuccessMessage from './success_message.vue';
+import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
+
+export default {
+ components: {
+ Actions,
+ LoadingButton,
+ CommitMessageField,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ isCompact: true,
+ componentHeight: null,
+ };
+ },
+ computed: {
+ ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapGetters(['hasChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ overviewText() {
+ return sprintf(
+ __(
+ '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
+ ),
+ {
+ stagedFilesLength: this.stagedFiles.length,
+ changedFilesLength: this.changedFiles.length,
+ },
+ );
+ },
+ },
+ watch: {
+ currentActivityView() {
+ if (this.lastCommitMsg) {
+ this.isCompact = false;
+ } else {
+ this.isCompact = !(
+ this.currentActivityView === activityBarViews.commit &&
+ window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
+ );
+ }
+ },
+ lastCommitMsg() {
+ this.isCompact =
+ this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
+ },
+ },
+ methods: {
+ ...mapActions(['updateActivityBarView']),
+ ...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
+ toggleIsSmall() {
+ this.updateActivityBarView(activityBarViews.commit)
+ .then(() => {
+ this.isCompact = !this.isCompact;
+ })
+ .catch(e => {
+ throw e;
+ });
+ },
+ beforeEnterTransition() {
+ const elHeight = this.isCompact
+ ? this.$refs.formEl && this.$refs.formEl.offsetHeight
+ : this.$refs.compactEl && this.$refs.compactEl.offsetHeight;
+
+ this.componentHeight = elHeight;
+ },
+ enterTransition() {
+ this.$nextTick(() => {
+ const elHeight = this.isCompact
+ ? this.$refs.compactEl && this.$refs.compactEl.offsetHeight
+ : this.$refs.formEl && this.$refs.formEl.offsetHeight;
+
+ this.componentHeight = elHeight;
+ });
+ },
+ afterEndTransition() {
+ this.componentHeight = null;
+ },
+ },
+ activityBarViews,
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-form"
+ :class="{
+ 'is-compact': isCompact,
+ 'is-full': !isCompact
+ }"
+ :style="{
+ height: componentHeight ? `${componentHeight}px` : null,
+ }"
+ >
+ <transition
+ name="commit-form-slide-up"
+ @before-enter="beforeEnterTransition"
+ @enter="enterTransition"
+ @after-enter="afterEndTransition"
+ >
+ <div
+ v-if="isCompact"
+ class="commit-form-compact"
+ ref="compactEl"
+ >
+ <button
+ type="button"
+ :disabled="!hasChanges"
+ class="btn btn-primary btn-sm btn-block"
+ @click="toggleIsSmall"
+ >
+ {{ __('Commit') }}
+ </button>
+ <p
+ class="text-center"
+ v-html="overviewText"
+ ></p>
+ </div>
+ <form
+ v-if="!isCompact"
+ class="form-horizontal"
+ @submit.prevent.stop="commitChanges"
+ ref="formEl"
+ >
+ <transition name="fade">
+ <success-message
+ v-show="lastCommitMsg"
+ />
+ </transition>
+ <commit-message-field
+ :text="commitMessage"
+ @input="updateCommitMessage"
+ />
+ <div class="clearfix prepend-top-15">
+ <actions />
+ <loading-button
+ :loading="submitCommitLoading"
+ :disabled="commitButtonDisabled"
+ container-class="btn btn-success btn-sm pull-left"
+ :label="__('Commit')"
+ @click="commitChanges"
+ />
+ <button
+ v-if="!discardDraftButtonDisabled"
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="discardDraft"
+ >
+ {{ __('Discard draft') }}
+ </button>
+ <button
+ v-else
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="toggleIsSmall"
+ >
+ {{ __('Collapse') }}
+ </button>
+ </div>
+ </form>
+ </transition>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 453208f3f19..c3ac18bfb83 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,66 +1,128 @@
<script>
- import { mapState } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
+import { mapActions } from 'vuex';
+import { __, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import ListItem from './list_item.vue';
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
+export default {
+ components: {
+ Icon,
+ ListItem,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
},
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
+ fileList: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- isCommitInfoShown() {
- return this.rightPanelCollapsed || this.fileList.length;
- },
+ iconName: {
+ type: String,
+ required: true,
},
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
+ action: {
+ type: String,
+ required: true,
},
- };
+ actionBtnText: {
+ type: String,
+ required: true,
+ },
+ itemActionComponent: {
+ type: String,
+ required: true,
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ showActionButton: false,
+ };
+ },
+ computed: {
+ titleText() {
+ return sprintf(__('%{title} changes'), {
+ title: this.title,
+ });
+ },
+ },
+ methods: {
+ ...mapActions(['stageAllChanges', 'unstageAllChanges']),
+ actionBtnClicked() {
+ this[this.action]();
+ },
+ setShowActionButton(show) {
+ this.showActionButton = show;
+ },
+ },
+};
</script>
<template>
<div
- :class="{
- 'multi-file-commit-list': isCommitInfoShown
- }"
+ class="ide-commit-list-container"
>
- <list-collapsed
- v-if="rightPanelCollapsed"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="list-unstyled append-bottom-0"
+ <header
+ class="multi-file-commit-panel-header"
+ @mouseenter="setShowActionButton(true)"
+ @mouseleave="setShowActionButton(false)"
+ >
+ <div
+ class="multi-file-commit-panel-header-title"
>
- <li
- v-for="file in fileList"
- :key="file.key"
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ {{ titleText }}
+ <span
+ v-show="!showActionButton"
+ class="ide-commit-file-count"
>
- <list-item
- :file="file"
- />
- </li>
- </ul>
- </template>
+ {{ fileList.length }}
+ </span>
+ <button
+ v-show="showActionButton"
+ type="button"
+ class="btn btn-blank btn-link ide-staged-action-btn"
+ @click="actionBtnClicked"
+ >
+ {{ actionBtnText }}
+ </button>
+ </div>
+ </header>
+ <ul
+ v-if="fileList.length"
+ class="multi-file-commit-list list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ :action-component="itemActionComponent"
+ :key-prefix="title"
+ :staged-list="stagedList"
+ />
+ </li>
+ </ul>
+ <p
+ v-else
+ class="multi-file-commit-list help-block"
+ >
+ {{ __('No changes') }}
+ </p>
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 15918ac9631..2254271c679 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -1,35 +1,110 @@
<script>
- import { mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { sprintf, n__, __ } from '~/locale';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
},
- computed: {
- ...mapGetters([
- 'addedFiles',
- 'modifiedFiles',
- ]),
+ iconName: {
+ type: String,
+ required: true,
},
- };
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ addedFilesLength() {
+ return this.files.filter(f => f.tempFile).length;
+ },
+ modifiedFilesLength() {
+ return this.files.filter(f => !f.tempFile).length;
+ },
+ addedFilesIconClass() {
+ return this.addedFilesLength ? 'multi-file-addition' : '';
+ },
+ modifiedFilesClass() {
+ return this.modifiedFilesLength ? 'multi-file-modified' : '';
+ },
+ additionsTooltip() {
+ return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
+ type: this.title.toLowerCase(),
+ });
+ },
+ modifiedTooltip() {
+ return sprintf(
+ n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
+ { type: this.title.toLowerCase() },
+ );
+ },
+ titleTooltip() {
+ return sprintf(__('%{title} changes'), { title: this.title });
+ },
+ additionIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
+ },
+ modifiedIconName() {
+ return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
+ },
+ },
+};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
- <icon
- name="file-addition"
- :size="18"
- css-classes="multi-file-addition append-bottom-10"
- />
- {{ addedFiles.length }}
- <icon
- name="file-modified"
- :size="18"
- css-classes="multi-file-modified prepend-top-10 append-bottom-10"
- />
- {{ modifiedFiles.length }}
+ <div
+ v-tooltip
+ :title="titleTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-15"
+ >
+ <icon
+ v-once
+ :name="iconName"
+ :size="18"
+ />
+ </div>
+ <div
+ v-tooltip
+ :title="additionsTooltip"
+ data-container="body"
+ data-placement="left"
+ class="append-bottom-10"
+ >
+ <icon
+ :name="additionIconName"
+ :size="18"
+ :css-classes="addedFilesIconClass"
+ />
+ </div>
+ {{ addedFilesLength }}
+ <div
+ v-tooltip
+ :title="modifiedTooltip"
+ data-container="body"
+ data-placement="left"
+ class="prepend-top-10 append-bottom-10"
+ >
+ <icon
+ :name="modifiedIconName"
+ :size="18"
+ :css-classes="modifiedFilesClass"
+ />
+ </div>
+ {{ modifiedFilesLength }}
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 560cdd941cd..03f3e4de83c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,34 +1,70 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
+import StageButton from './stage_button.vue';
+import UnstageButton from './unstage_button.vue';
+import { viewerTypes } from '../../constants';
export default {
components: {
Icon,
+ StageButton,
+ UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
+ actionComponent: {
+ type: String,
+ required: true,
+ },
+ keyPrefix: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ stagedList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
+ const prefix = this.stagedList ? '-solid' : '';
+ return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
},
methods: {
- ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
- openFileInEditor(file) {
- return this.openPendingTab(file).then(changeViewer => {
+ ...mapActions([
+ 'discardFileChanges',
+ 'updateViewer',
+ 'openPendingTab',
+ 'unstageChange',
+ 'stageChange',
+ ]),
+ openFileInEditor() {
+ return this.openPendingTab({
+ file: this.file,
+ keyPrefix: this.keyPrefix.toLowerCase(),
+ }).then(changeViewer => {
if (changeViewer) {
- this.updateViewer('diff');
+ this.updateViewer(viewerTypes.diff);
}
});
},
+ fileAction() {
+ if (this.file.staged) {
+ this.unstageChange(this.file.path);
+ } else {
+ this.stageChange(this.file.path);
+ }
+ },
},
};
</script>
@@ -38,7 +74,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
- @click="openFileInEditor(file)">
+ @dblclick="fileAction"
+ @click="openFileInEditor"
+ >
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
@@ -47,12 +85,9 @@ export default {
/>{{ file.path }}
</span>
</button>
- <button
- type="button"
- class="btn btn-blank multi-file-discard-btn"
- @click="discardFileChanges(file.path)"
- >
- Discard
- </button>
+ <component
+ :is="actionComponent"
+ :path="file.path"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index b660a2961cb..00f2312ae51 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
+import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
@@ -26,10 +27,20 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
+ tooltipTitle() {
+ return this.disabled
+ ? __('This option is disabled while you still have unstaged changes')
+ : '';
+ },
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
@@ -39,19 +50,28 @@ export default {
<template>
<fieldset>
- <label>
+ <label
+ v-tooltip
+ :title="tooltipTitle"
+ :class="{
+ 'is-disabled': disabled
+ }"
+ >
<input
type="radio"
name="commit-action"
:value="value"
@change="updateCommitAction($event.target.value)"
- :checked="checked"
- v-once
+ :checked="commitAction === value"
+ :disabled="disabled"
/>
<span class="prepend-left-10">
- <template v-if="label">
+ <span
+ v-if="label"
+ class="ide-radio-label"
+ >
{{ label }}
- </template>
+ </span>
<slot v-else></slot>
</span>
</label>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
new file mode 100644
index 00000000000..52dce8412ab
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -0,0 +1,59 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['stageChange', 'discardFileChanges']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank append-right-5"
+ :aria-label="__('Stage changes')"
+ :title="__('Stage changes')"
+ data-container="body"
+ @click.stop="stageChange(path)"
+ >
+ <icon
+ name="mobile-issue-close"
+ :size="12"
+ />
+ </button>
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Discard changes')"
+ :title="__('Discard changes')"
+ data-container="body"
+ @click.stop="discardFileChanges(path)"
+ >
+ <icon
+ name="remove"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
new file mode 100644
index 00000000000..a6df91b79c2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -0,0 +1,33 @@
+<script>
+import { mapState } from 'vuex';
+
+export default {
+ computed: {
+ ...mapState(['lastCommitMsg', 'committedStateSvgPath']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-success-message"
+ aria-live="assertive"
+ >
+ <div class="svg-content svg-80">
+ <img
+ :src="committedStateSvgPath"
+ alt=""
+ />
+ </div>
+ <div class="append-right-default prepend-left-default">
+ <div
+ class="text-content text-center"
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg"></p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
new file mode 100644
index 00000000000..123d60da47e
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ ...mapActions(['unstageChange']),
+ },
+};
+</script>
+
+<template>
+ <div
+ v-once
+ class="multi-file-discard-btn"
+ >
+ <button
+ v-tooltip
+ type="button"
+ class="btn btn-blank"
+ :aria-label="__('Unstage changes')"
+ :title="__('Unstage changes')"
+ data-container="body"
+ @click="unstageChange(path)"
+ >
+ <icon
+ name="history"
+ :size="12"
+ />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 0c44a755f56..b9af4d27145 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,28 +1,15 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
+import { viewerTypes } from '../constants';
export default {
- components: {
- Icon,
- },
props: {
- hasChanges: {
- type: Boolean,
- required: false,
- default: false,
- },
- mergeRequestId: {
- type: String,
- required: false,
- default: '',
- },
viewer: {
type: String,
required: true,
},
- showShadow: {
- type: Boolean,
+ mergeRequestId: {
+ type: Number,
required: true,
},
},
@@ -38,84 +25,45 @@ export default {
this.$emit('click', mode);
},
},
+ viewerTypes,
};
</script>
<template>
<div
class="dropdown"
- :class="{
- shadow: showShadow,
- }"
>
<button
type="button"
- class="btn btn-primary btn-sm"
- :class="{
- 'btn-inverted': hasChanges,
- }"
+ class="btn btn-link"
data-toggle="dropdown"
>
- <template v-if="viewer === 'mrdiff' && mergeRequestId">
- {{ mergeReviewLine }}
- </template>
- <template v-else-if="viewer === 'editor'">
- {{ __('Editing') }}
- </template>
- <template v-else>
- {{ __('Reviewing') }}
- </template>
- <icon
- name="angle-down"
- :size="12"
- css-classes="caret-down"
- />
+ {{ __('Edit') }}
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
- <template v-if="mergeRequestId">
- <li>
- <a
- href="#"
- @click.prevent="changeMode('mrdiff')"
- :class="{
- 'is-active': viewer === 'mrdiff',
- }"
- >
- <strong class="dropdown-menu-inner-title">
- {{ mergeReviewLine }}
- </strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the merge request target branch') }}
- </span>
- </a>
- </li>
- <li
- role="separator"
- class="divider"
- >
- </li>
- </template>
<li>
<a
href="#"
- @click.prevent="changeMode('editor')"
+ @click.prevent="changeMode($options.viewerTypes.mr)"
:class="{
- 'is-active': viewer === 'editor',
+ 'is-active': viewer === $options.viewerTypes.mr,
}"
>
- <strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
+ <strong class="dropdown-menu-inner-title">
+ {{ mergeReviewLine }}
+ </strong>
<span class="dropdown-menu-inner-content">
- {{ __('View and edit lines') }}
+ {{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li>
<a
href="#"
- @click.prevent="changeMode('diff')"
+ @click.prevent="changeMode($options.viewerTypes.diff)"
:class="{
- 'is-active': viewer === 'diff',
+ 'is-active': viewer === $options.viewerTypes.diff,
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
new file mode 100644
index 00000000000..cabb3f59b17
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -0,0 +1,243 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import VirtualList from 'vue-virtual-scroll-list';
+import Item from './item.vue';
+import router from '../../ide_router';
+import {
+ MAX_FILE_FINDER_RESULTS,
+ FILE_FINDER_ROW_HEIGHT,
+ FILE_FINDER_EMPTY_ROW_HEIGHT,
+} from '../../constants';
+import {
+ UP_KEY_CODE,
+ DOWN_KEY_CODE,
+ ENTER_KEY_CODE,
+ ESC_KEY_CODE,
+} from '../../../lib/utils/keycodes';
+
+export default {
+ components: {
+ Item,
+ VirtualList,
+ },
+ data() {
+ return {
+ focusedIndex: 0,
+ searchText: '',
+ mouseOver: false,
+ cancelMouseOver: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['allBlobs']),
+ ...mapState(['fileFindVisible', 'loading']),
+ filteredBlobs() {
+ const searchText = this.searchText.trim();
+
+ if (searchText === '') {
+ return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
+ }
+
+ return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ });
+ },
+ filteredBlobsLength() {
+ return this.filteredBlobs.length;
+ },
+ listShowCount() {
+ return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
+ },
+ listHeight() {
+ return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
+ },
+ showClearInputButton() {
+ return this.searchText.trim() !== '';
+ },
+ },
+ watch: {
+ fileFindVisible() {
+ this.$nextTick(() => {
+ if (!this.fileFindVisible) {
+ this.searchText = '';
+ } else {
+ this.focusedIndex = 0;
+
+ if (this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ }
+ });
+ },
+ searchText() {
+ this.focusedIndex = 0;
+ },
+ focusedIndex() {
+ if (!this.mouseOver) {
+ this.$nextTick(() => {
+ const el = this.$refs.virtualScrollList.$el;
+ const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
+ const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
+
+ if (this.focusedIndex === 0) {
+ // if index is the first index, scroll straight to start
+ el.scrollTop = 0;
+ } else if (this.focusedIndex === this.filteredBlobsLength - 1) {
+ // if index is the last index, scroll to the end
+ el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop >= bottom + el.scrollTop) {
+ // if element is off the bottom of the scroll list, scroll down one item
+ el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
+ } else if (scrollTop < el.scrollTop) {
+ // if element is off the top of the scroll list, scroll up one item
+ el.scrollTop = scrollTop;
+ }
+ });
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ clearSearchInput() {
+ this.searchText = '';
+
+ this.$nextTick(() => {
+ this.$refs.searchInput.focus();
+ });
+ },
+ onKeydown(e) {
+ switch (e.keyCode) {
+ case UP_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex > 0) {
+ this.focusedIndex -= 1;
+ } else {
+ this.focusedIndex = this.filteredBlobsLength - 1;
+ }
+ break;
+ case DOWN_KEY_CODE:
+ e.preventDefault();
+ this.mouseOver = false;
+ this.cancelMouseOver = true;
+ if (this.focusedIndex < this.filteredBlobsLength - 1) {
+ this.focusedIndex += 1;
+ } else {
+ this.focusedIndex = 0;
+ }
+ break;
+ default:
+ break;
+ }
+ },
+ onKeyup(e) {
+ switch (e.keyCode) {
+ case ENTER_KEY_CODE:
+ this.openFile(this.filteredBlobs[this.focusedIndex]);
+ break;
+ case ESC_KEY_CODE:
+ this.toggleFileFinder(false);
+ break;
+ default:
+ break;
+ }
+ },
+ openFile(file) {
+ this.toggleFileFinder(false);
+ router.push(`/project${file.url}`);
+ },
+ onMouseOver(index) {
+ if (!this.cancelMouseOver) {
+ this.mouseOver = true;
+ this.focusedIndex = index;
+ }
+ },
+ onMouseMove(index) {
+ this.cancelMouseOver = false;
+ this.onMouseOver(index);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-finder-overlay"
+ @mousedown.self="toggleFileFinder(false)"
+ >
+ <div
+ class="dropdown-menu diff-file-changes ide-file-finder show"
+ >
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ :placeholder="__('Search files')"
+ autocomplete="off"
+ v-model="searchText"
+ ref="searchInput"
+ @keydown="onKeydown($event)"
+ @keyup="onKeyup($event)"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ :class="{
+ hidden: showClearInputButton
+ }"
+ ></i>
+ <i
+ role="button"
+ :aria-label="__('Clear search input')"
+ class="fa fa-times dropdown-input-clear"
+ :class="{
+ show: showClearInputButton
+ }"
+ @click="clearSearchInput"
+ ></i>
+ </div>
+ <div>
+ <virtual-list
+ :size="listHeight"
+ :remain="listShowCount"
+ wtag="ul"
+ ref="virtualScrollList"
+ >
+ <template v-if="filteredBlobsLength">
+ <li
+ v-for="(file, index) in filteredBlobs"
+ :key="file.key"
+ >
+ <item
+ class="disable-hover"
+ :file="file"
+ :search-text="searchText"
+ :focused="index === focusedIndex"
+ :index="index"
+ @click="openFile"
+ @mouseover="onMouseOver"
+ @mousemove="onMouseMove"
+ />
+ </li>
+ </template>
+ <li
+ v-else
+ class="dropdown-menu-empty-item"
+ >
+ <div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
+ <template v-if="loading">
+ {{ __('Loading...') }}
+ </template>
+ <template v-else>
+ {{ __('No files found.') }}
+ </template>
+ </div>
+ </li>
+ </virtual-list>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue
new file mode 100644
index 00000000000..d4427420207
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/item.vue
@@ -0,0 +1,113 @@
+<script>
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import FileIcon from '../../../vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '../changed_file_icon.vue';
+
+const MAX_PATH_LENGTH = 60;
+
+export default {
+ components: {
+ ChangedFileIcon,
+ FileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ focused: {
+ type: Boolean,
+ required: true,
+ },
+ searchText: {
+ type: String,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ pathWithEllipsis() {
+ const path = this.file.path;
+
+ return path.length < MAX_PATH_LENGTH
+ ? path
+ : `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
+ },
+ nameSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.file.name, this.searchText);
+ },
+ pathSearchTextOccurences() {
+ return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
+ },
+ },
+ methods: {
+ clickRow() {
+ this.$emit('click', this.file);
+ },
+ mouseOverRow() {
+ this.$emit('mouseover', this.index);
+ },
+ mouseMove() {
+ this.$emit('mousemove', this.index);
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ class="diff-changed-file"
+ :class="{
+ 'is-focused': focused,
+ }"
+ @click.prevent="clickRow"
+ @mouseover="mouseOverRow"
+ @mousemove="mouseMove"
+ >
+ <file-icon
+ :file-name="file.name"
+ :size="16"
+ css-classes="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ class="diff-changed-file-name"
+ >
+ <span
+ v-for="(char, index) in file.name.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </strong>
+ <span
+ class="diff-changed-file-path prepend-top-5"
+ >
+ <span
+ v-for="(char, index) in pathWithEllipsis.split('')"
+ :key="index + char"
+ :class="{
+ highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
+ }"
+ v-text="char"
+ >
+ </span>
+ </span>
+ </span>
+ <span
+ v-if="file.changed || file.tempFile"
+ class="diff-changed-stats"
+ >
+ <changed-file-icon
+ :file="file"
+ />
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1c237c0ec97..2c184ea726c 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,35 +1,31 @@
<script>
-import { mapState, mapGetters } from 'vuex';
-import ideSidebar from './ide_side_bar.vue';
-import ideContextbar from './ide_context_bar.vue';
-import repoTabs from './repo_tabs.vue';
-import ideStatusBar from './ide_status_bar.vue';
-import repoEditor from './repo_editor.vue';
+import Mousetrap from 'mousetrap';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import IdeSidebar from './ide_side_bar.vue';
+import RepoTabs from './repo_tabs.vue';
+import IdeStatusBar from './ide_status_bar.vue';
+import RepoEditor from './repo_editor.vue';
+import FindFile from './file_finder/index.vue';
+
+const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- ideStatusBar,
- repoEditor,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
+ IdeSidebar,
+ RepoTabs,
+ IdeStatusBar,
+ RepoEditor,
+ FindFile,
},
computed: {
- ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
+ ...mapState([
+ 'changedFiles',
+ 'openFiles',
+ 'viewer',
+ 'currentMergeRequestId',
+ 'fileFindVisible',
+ 'emptyStateSvgPath',
+ ]),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
@@ -42,67 +38,91 @@ export default {
});
return returnValue;
};
+
+ Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
+ this.toggleFileFinder(!this.fileFindVisible);
+ });
+
+ Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ mousetrapStopCallback(e, el, combo) {
+ if (
+ (combo === 't' && el.classList.contains('dropdown-input-field')) ||
+ el.classList.contains('inputarea')
+ ) {
+ return true;
+ } else if (combo === 'command+p' || combo === 'ctrl+p') {
+ return false;
+ }
+
+ return originalStopCallback(e, el, combo);
+ },
},
};
</script>
<template>
- <div
- class="ide-view"
- >
- <ide-sidebar />
+ <article class="ide">
<div
- class="multi-file-edit-pane"
+ class="ide-view"
>
- <template
- v-if="activeFile"
+ <find-file
+ v-show="fileFindVisible"
+ />
+ <ide-sidebar />
+ <div
+ class="multi-file-edit-pane"
>
- <repo-tabs
- :active-file="activeFile"
- :files="openFiles"
- :viewer="viewer"
- :has-changes="hasChanges"
- :merge-request-id="currentMergeRequestId"
- />
- <repo-editor
- class="multi-file-edit-pane-content"
- :file="activeFile"
- />
- <ide-status-bar
- :file="activeFile"
- />
- </template>
- <template
- v-else
- >
- <div
- v-once
- class="ide-empty-state"
+ <template
+ v-if="activeFile"
+ >
+ <repo-tabs
+ :active-file="activeFile"
+ :files="openFiles"
+ :viewer="viewer"
+ :has-changes="hasChanges"
+ :merge-request-id="currentMergeRequestId"
+ />
+ <repo-editor
+ class="multi-file-edit-pane-content"
+ :file="activeFile"
+ />
+ </template>
+ <template
+ v-else
>
- <div class="row js-empty-state">
- <div class="col-xs-12">
- <div class="svg-content svg-250">
- <img :src="emptyStateSvgPath" />
+ <div
+ v-once
+ class="ide-empty-state"
+ >
+ <div class="row js-empty-state">
+ <div class="col-12">
+ <div class="svg-content svg-250">
+ <img :src="emptyStateSvgPath" />
+ </div>
</div>
- </div>
- <div class="col-xs-12">
- <div class="text-content text-center">
- <h4>
- Welcome to the GitLab IDE
- </h4>
- <p>
- You can select a file in the left sidebar to begin
- editing and use the right sidebar to commit your changes.
- </p>
+ <div class="col-12">
+ <div class="text-content text-center">
+ <h4>
+ Welcome to the GitLab IDE
+ </h4>
+ <p>
+ Select a file from the left sidebar to begin editing.
+ Afterwards, you'll be able to commit your changes.
+ </p>
+ </div>
</div>
</div>
</div>
- </div>
- </template>
+ </template>
+ </div>
</div>
- <ide-contextbar
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
- />
- </div>
+ <ide-status-bar :file="activeFile"/>
+ </article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
deleted file mode 100644
index 79a83b47994..00000000000
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
-import icon from '~/vue_shared/components/icon.vue';
-import panelResizer from '~/vue_shared/components/panel_resizer.vue';
-import repoCommitSection from './repo_commit_section.vue';
-import ResizablePanel from './resizable_panel.vue';
-
-export default {
- components: {
- repoCommitSection,
- icon,
- panelResizer,
- ResizablePanel,
- },
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState(['changedFiles', 'rightPanelCollapsed']),
- ...mapGetters(['currentIcon']),
- },
- methods: {
- ...mapActions(['setPanelCollapsedStatus']),
- },
-};
-</script>
-
-<template>
- <resizable-panel
- :collapsible="true"
- :initial-width="340"
- side="right"
- >
- <div
- class="multi-file-commit-panel-section"
- >
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- >
- <div
- class="multi-file-commit-panel-header-title"
- v-if="!rightPanelCollapsed"
- >
- <div
- v-if="changedFiles.length"
- >
- <icon
- name="list-bulleted"
- :size="18"
- />
- Staged
- </div>
- </div>
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click.stop="setPanelCollapsedStatus({
- side: 'right',
- collapsed: !rightPanelCollapsed,
- })"
- >
- <icon
- :name="currentIcon"
- :size="18"
- />
- </button>
- </header>
- <repo-commit-section
- :no-changes-state-svg-path="noChangesStateSvgPath"
- :committed-state-svg-path="committedStateSvgPath"
- />
- </div>
- </resizable-panel>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue
deleted file mode 100644
index c6f6e0d2348..00000000000
--- a/app/assets/javascripts/ide/components/ide_external_links.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- icon,
- },
- props: {
- projectUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- goBackUrl() {
- return document.referrer || this.projectUrl;
- },
- },
-};
-</script>
-
-<template>
- <nav
- class="ide-external-links"
- v-once
- >
- <p>
- <a
- :href="goBackUrl"
- class="ide-sidebar-link"
- >
- <icon
- :size="16"
- class="append-right-8"
- name="go-back"
- />
- <span class="ide-external-links-text">
- {{ s__('Go back') }}
- </span>
- </a>
- </p>
- </nav>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_file_buttons.vue b/app/assets/javascripts/ide/components/ide_file_buttons.vue
index a6c6f46a144..30b00abf6ed 100644
--- a/app/assets/javascripts/ide/components/ide_file_buttons.vue
+++ b/app/assets/javascripts/ide/components/ide_file_buttons.vue
@@ -32,14 +32,14 @@ export default {
<template>
<div
v-if="showButtons"
- class="pull-right ide-btn-group"
+ class="float-right ide-btn-group"
>
<a
v-tooltip
v-if="!file.binary"
:href="file.blamePath"
:title="__('Blame')"
- class="btn btn-xs btn-transparent blame"
+ class="btn btn-sm btn-transparent blame"
>
<icon
name="blame"
@@ -50,7 +50,7 @@ export default {
v-tooltip
:href="file.commitsPath"
:title="__('History')"
- class="btn btn-xs btn-transparent history"
+ class="btn btn-sm btn-transparent history"
>
<icon
name="history"
@@ -61,7 +61,7 @@ export default {
v-tooltip
:href="file.permalink"
:title="__('Permalink')"
- class="btn btn-xs btn-transparent permalink"
+ class="btn btn-sm btn-transparent permalink"
>
<icon
name="link"
@@ -72,7 +72,7 @@ export default {
v-tooltip
:href="file.rawPath"
target="_blank"
- class="btn btn-xs btn-transparent prepend-left-10 raw"
+ class="btn btn-sm btn-transparent prepend-left-10 raw"
rel="noopener noreferrer"
:title="rawDownloadButtonLabel">
<icon
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
deleted file mode 100644
index eb2749e6151..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
- import icon from '~/vue_shared/components/icon.vue';
- import repoTree from './ide_repo_tree.vue';
- import newDropdown from './new_dropdown/index.vue';
-
- export default {
- components: {
- repoTree,
- icon,
- newDropdown,
- },
- props: {
- projectId: {
- type: String,
- required: true,
- },
- branch: {
- type: Object,
- required: true,
- },
- },
- };
-</script>
-
-<template>
- <div class="branch-container">
- <div class="branch-header">
- <div class="branch-header-title str-truncated ref-name">
- <icon
- name="branch"
- :size="12"
- />
- {{ branch.name }}
- </div>
- <div class="branch-header-btns">
- <new-dropdown
- :project-id="projectId"
- :branch="branch.name"
- path=""
- />
- </div>
- </div>
- <repo-tree
- :tree="branch.tree"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
deleted file mode 100644
index a6f40286ac1..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_tree.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
-import Identicon from '../../vue_shared/components/identicon.vue';
-import BranchesTree from './ide_project_branches_tree.vue';
-import ExternalLinks from './ide_external_links.vue';
-
-export default {
- components: {
- BranchesTree,
- ExternalLinks,
- ProjectAvatarImage,
- Identicon,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="projects-sidebar">
- <div class="context-header">
- <a
- :title="project.name"
- :href="project.web_url"
- >
- <div
- v-if="project.avatar_url"
- class="avatar-container s40 project-avatar"
- >
- <project-avatar-image
- class="avatar-container project-avatar"
- :link-href="project.path"
- :img-src="project.avatar_url"
- :img-alt="project.name"
- :img-size="40"
- />
- </div>
- <identicon
- v-else
- size-class="s40"
- :entity-id="project.id"
- :entity-name="project.name"
- />
- <div class="sidebar-context-title">
- {{ project.name }}
- </div>
- </a>
- </div>
- <external-links
- :project-url="project.web_url"
- />
- <div class="multi-file-commit-panel-inner-scroll">
- <branches-tree
- v-for="branch in project.branches"
- :key="branch.name"
- :project-id="project.path_with_namespace"
- :branch="branch"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
deleted file mode 100644
index e6af88e04bc..00000000000
--- a/app/assets/javascripts/ide/components/ide_repo_tree.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import RepoFile from './repo_file.vue';
-
-export default {
- components: {
- RepoFile,
- SkeletonLoadingContainer,
- },
- props: {
- tree: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div
- class="ide-file-list"
- >
- <template v-if="tree.loading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <template v-else>
- <repo-file
- v-for="file in tree.tree"
- :key="file.key"
- :file="file"
- :level="0"
- />
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
new file mode 100644
index 00000000000..0c9ec3b00f0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapGetters, mapState, mapActions } from 'vuex';
+import IdeTreeList from './ide_tree_list.vue';
+import EditorModeDropdown from './editor_mode_dropdown.vue';
+import { viewerTypes } from '../constants';
+
+export default {
+ components: {
+ IdeTreeList,
+ EditorModeDropdown,
+ },
+ computed: {
+ ...mapGetters(['currentMergeRequest']),
+ ...mapState(['viewer']),
+ showLatestChangesText() {
+ return !this.currentMergeRequest || this.viewer === viewerTypes.diff;
+ },
+ showMergeRequestText() {
+ return this.currentMergeRequest && this.viewer === viewerTypes.mr;
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff);
+ });
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ :viewer-type="viewer"
+ header-class="ide-review-header"
+ :disable-action-dropdown="true"
+ >
+ <template
+ slot="header"
+ >
+ <div class="ide-review-button-holder">
+ {{ __('Review') }}
+ <editor-mode-dropdown
+ v-if="currentMergeRequest"
+ :viewer="viewer"
+ :merge-request-id="currentMergeRequest.iid"
+ @click="updateViewer"
+ />
+ </div>
+ <div class="prepend-top-5 ide-review-sub-header">
+ <template v-if="showLatestChangesText">
+ {{ __('Latest changes') }}
+ </template>
+ <template v-else-if="showMergeRequestText">
+ {{ __('Merge request') }}
+ (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>)
+ </template>
+ </div>
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 8cf1ccb4fce..3f980203911 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -1,36 +1,82 @@
<script>
- import { mapState, mapGetters } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import panelResizer from '~/vue_shared/components/panel_resizer.vue';
- import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
- import projectTree from './ide_project_tree.vue';
- import ResizablePanel from './resizable_panel.vue';
+import { mapState, mapGetters } from 'vuex';
+import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import Identicon from '../../vue_shared/components/identicon.vue';
+import IdeTree from './ide_tree.vue';
+import ResizablePanel from './resizable_panel.vue';
+import ActivityBar from './activity_bar.vue';
+import CommitSection from './repo_commit_section.vue';
+import CommitForm from './commit_sidebar/form.vue';
+import IdeReview from './ide_review.vue';
+import SuccessMessage from './commit_sidebar/success_message.vue';
+import { activityBarViews } from '../constants';
- export default {
- components: {
- projectTree,
- icon,
- panelResizer,
- skeletonLoadingContainer,
- ResizablePanel,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ Icon,
+ PanelResizer,
+ SkeletonLoadingContainer,
+ ResizablePanel,
+ ActivityBar,
+ ProjectAvatarImage,
+ Identicon,
+ CommitSection,
+ IdeTree,
+ CommitForm,
+ IdeReview,
+ SuccessMessage,
+ },
+ data() {
+ return {
+ showTooltip: false,
+ };
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'currentBranchId',
+ 'currentActivityView',
+ 'changedFiles',
+ 'stagedFiles',
+ 'lastCommitMsg',
+ ]),
+ ...mapGetters(['currentProject', 'someUncommitedChanges']),
+ showSuccessMessage() {
+ return (
+ this.currentActivityView === activityBarViews.edit &&
+ (this.lastCommitMsg && !this.someUncommitedChanges)
+ );
},
- computed: {
- ...mapState([
- 'loading',
- ]),
- ...mapGetters([
- 'projectsWithTrees',
- ]),
+ branchTooltipTitle() {
+ return this.showTooltip ? this.currentBranchId : undefined;
},
- };
+ },
+ watch: {
+ currentBranchId() {
+ this.$nextTick(() => {
+ this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
+ });
+ },
+ },
+};
</script>
<template>
<resizable-panel
:collapsible="false"
- :initial-width="290"
+ :initial-width="340"
side="left"
>
+ <activity-bar
+ v-if="!loading"
+ />
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<div
@@ -41,11 +87,54 @@
<skeleton-loading-container />
</div>
</template>
- <project-tree
- v-for="project in projectsWithTrees"
- :key="project.id"
- :project="project"
- />
+ <template v-else>
+ <div class="context-header ide-context-header">
+ <a
+ :href="currentProject.web_url"
+ >
+ <div
+ v-if="currentProject.avatar_url"
+ class="avatar-container s40 project-avatar"
+ >
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="currentProject.path"
+ :img-src="currentProject.avatar_url"
+ :img-alt="currentProject.name"
+ :img-size="40"
+ />
+ </div>
+ <identicon
+ v-else
+ size-class="s40"
+ :entity-id="currentProject.id"
+ :entity-name="currentProject.name"
+ />
+ <div class="ide-sidebar-project-title">
+ <div class="sidebar-context-title">
+ {{ currentProject.name }}
+ </div>
+ <div
+ class="sidebar-context-title ide-sidebar-branch-title"
+ ref="branchId"
+ v-tooltip
+ :title="branchTooltipTitle"
+ >
+ <icon
+ name="branch"
+ css-classes="append-right-5"
+ />{{ currentBranchId }}
+ </div>
+ </div>
+ </a>
+ </div>
+ <div class="multi-file-commit-panel-inner-scroll">
+ <component
+ :is="currentActivityView"
+ />
+ </div>
+ <commit-form />
+ </template>
</div>
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 152a5f632ad..6f60cfbf184 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -1,11 +1,16 @@
<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
+import CiIcon from '../../vue_shared/components/ci_icon.vue';
+import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
components: {
icon,
+ userAvatarImage,
+ CiIcon,
},
directives: {
tooltip,
@@ -14,47 +19,126 @@ export default {
props: {
file: {
type: Object,
- required: true,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ lastCommitFormatedAge: null,
+ };
+ },
+ computed: {
+ ...mapState(['currentBranchId', 'currentProjectId']),
+ ...mapGetters(['currentProject', 'lastCommit']),
+ },
+ watch: {
+ lastCommit() {
+ if (!this.isPollingInitialized) {
+ this.initPipelinePolling();
+ }
+ },
+ },
+ mounted() {
+ this.startTimer();
+ },
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ if (this.isPollingInitialized) {
+ this.stopPipelinePolling();
+ }
+ },
+ methods: {
+ ...mapActions(['pipelinePoll', 'stopPipelinePolling']),
+ startTimer() {
+ this.intervalId = setInterval(() => {
+ this.commitAgeUpdate();
+ }, 1000);
+ },
+ initPipelinePolling() {
+ this.pipelinePoll();
+ this.isPollingInitialized = true;
+ },
+ commitAgeUpdate() {
+ if (this.lastCommit) {
+ this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
+ }
+ },
+ getCommitPath(shortSha) {
+ return `${this.currentProject.web_url}/commit/${shortSha}`;
},
},
};
</script>
<template>
- <div class="ide-status-bar">
- <div class="ref-name">
+ <footer class="ide-status-bar">
+ <div
+ class="ide-status-branch"
+ v-if="lastCommit && lastCommitFormatedAge"
+ >
+ <span
+ class="ide-status-pipeline"
+ v-if="lastCommit.pipeline && lastCommit.pipeline.details"
+ >
+ <ci-icon
+ :status="lastCommit.pipeline.details.status"
+ v-tooltip
+ :title="lastCommit.pipeline.details.status.text"
+ />
+ Pipeline
+ <a
+ class="monospace"
+ :href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a>
+ {{ lastCommit.pipeline.details.status.text }}
+ for
+ </span>
+
<icon
- name="branch"
- :size="12"
+ name="commit"
/>
- {{ file.branchId }}
- </div>
- <div>
- <div v-if="file.lastCommit && file.lastCommit.id">
- Last commit:
- <a
- v-tooltip
- :title="file.lastCommit.message"
- :href="file.lastCommit.url"
- >
- {{ timeFormated(file.lastCommit.updatedAt) }} by
- {{ file.lastCommit.author }}
- </a>
- </div>
+ <a
+ v-tooltip
+ class="commit-sha"
+ :title="lastCommit.message"
+ :href="getCommitPath(lastCommit.short_id)"
+ >{{ lastCommit.short_id }}</a>
+ by
+ {{ lastCommit.author_name }}
+ <time
+ v-tooltip
+ data-placement="top"
+ data-container="body"
+ :datetime="lastCommit.committed_date"
+ :title="tooltipTitle(lastCommit.committed_date)"
+ >
+ {{ lastCommitFormatedAge }}
+ </time>
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.name }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.eol }}
</div>
<div
- class="text-right"
- v-if="!file.binary">
+ class="ide-status-file"
+ v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }}
</div>
- <div class="text-right">
+ <div
+ v-if="file"
+ class="ide-status-file"
+ >
{{ file.fileLanguage }}
</div>
- </div>
+ </footer>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
new file mode 100644
index 00000000000..8fc4ebe6ca6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -0,0 +1,42 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import NewDropdown from './new_dropdown/index.vue';
+import IdeTreeList from './ide_tree_list.vue';
+
+export default {
+ components: {
+ NewDropdown,
+ IdeTreeList,
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree', 'activeFile']),
+ },
+ mounted() {
+ if (this.activeFile && this.activeFile.pending) {
+ this.$router.push(`/project${this.activeFile.url}`, () => {
+ this.updateViewer('editor');
+ });
+ }
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <ide-tree-list
+ viewer-type="editor"
+ >
+ <template
+ slot="header"
+ >
+ {{ __('Edit') }}
+ <new-dropdown
+ :project-id="currentProject.name_with_namespace"
+ :branch="currentBranchId"
+ />
+ </template>
+ </ide-tree-list>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
new file mode 100644
index 00000000000..e64a09fcc90
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -0,0 +1,76 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import RepoFile from './repo_file.vue';
+import NewDropdown from './new_dropdown/index.vue';
+
+export default {
+ components: {
+ Icon,
+ RepoFile,
+ SkeletonLoadingContainer,
+ NewDropdown,
+ },
+ props: {
+ viewerType: {
+ type: String,
+ required: true,
+ },
+ headerClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState(['currentBranchId']),
+ ...mapGetters(['currentProject', 'currentTree']),
+ showLoading() {
+ return !this.currentTree || this.currentTree.loading;
+ },
+ },
+ mounted() {
+ this.updateViewer(this.viewerType);
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-list"
+ >
+ <template v-if="showLoading">
+ <div
+ class="multi-file-loading-container"
+ v-for="n in 3"
+ :key="n"
+ >
+ <skeleton-loading-container />
+ </div>
+ </template>
+ <template v-else>
+ <header
+ class="ide-tree-header"
+ :class="headerClass"
+ >
+ <slot name="header"></slot>
+ </header>
+ <repo-file
+ v-for="file in currentTree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :disable-action-dropdown="disableActionDropdown"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
index 8a440902dfc..179a589d1ac 100644
--- a/app/assets/javascripts/ide/components/mr_file_icon.vue
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -16,8 +16,8 @@ export default {
<icon
name="git-merge"
v-tooltip
- title="__('Part of merge request changes')"
- css-classes="ide-file-changed-icon"
+ :title="__('Part of merge request changes')"
+ css-classes="append-right-8"
:size="12"
/>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 769e9b79cad..f0b29702497 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -1,49 +1,55 @@
<script>
- import { mapActions } from 'vuex';
- import icon from '~/vue_shared/components/icon.vue';
- import newModal from './modal.vue';
- import upload from './upload.vue';
+import { mapActions } from 'vuex';
+import icon from '~/vue_shared/components/icon.vue';
+import newModal from './modal.vue';
+import upload from './upload.vue';
- export default {
- components: {
- icon,
- newModal,
- upload,
+export default {
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ props: {
+ branch: {
+ type: String,
+ required: true,
},
- props: {
- branch: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
+ path: {
+ type: String,
+ required: false,
+ default: '',
},
- data() {
- return {
- openModal: false,
- modalType: '',
- dropdownOpen: false,
- };
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ dropdownOpen: false,
+ };
+ },
+ watch: {
+ dropdownOpen() {
+ this.$nextTick(() => {
+ this.$refs.dropdownMenu.scrollIntoView();
+ });
},
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createNewItem(type) {
- this.modalType = type;
- this.openModal = true;
- this.dropdownOpen = false;
- },
- hideModal() {
- this.openModal = false;
- },
- openDropdown() {
- this.dropdownOpen = !this.dropdownOpen;
- },
+ },
+ methods: {
+ ...mapActions(['createTempEntry']),
+ createNewItem(type) {
+ this.modalType = type;
+ this.openModal = true;
+ this.dropdownOpen = false;
},
- };
+ hideModal() {
+ this.openModal = false;
+ },
+ openDropdown() {
+ this.dropdownOpen = !this.dropdownOpen;
+ },
+ },
+};
</script>
<template>
@@ -51,7 +57,7 @@
<div
class="dropdown"
:class="{
- open: dropdownOpen,
+ show: dropdownOpen,
}"
>
<button
@@ -63,15 +69,18 @@
<icon
name="plus"
:size="12"
- css-classes="pull-left"
+ css-classes="float-left"
/>
<icon
name="arrow-down"
:size="12"
- css-classes="pull-left"
+ css-classes="float-left"
/>
</button>
- <ul class="dropdown-menu dropdown-menu-right">
+ <ul
+ class="dropdown-menu dropdown-menu-right"
+ ref="dropdownMenu"
+ >
<li>
<a
href="#"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 4b5a50785b6..d83a90f71e1 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -40,13 +40,6 @@ export default {
return __('Create file');
},
- formLabelName() {
- if (this.type === 'tree') {
- return __('Directory name');
- }
-
- return __('File name');
- },
},
mounted() {
this.$refs.fieldName.focus();
@@ -77,13 +70,13 @@ export default {
@submit="createEntryInStore"
>
<form
- class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
+ class="form-group row append-bottom-0"
>
<fieldset class="form-group append-bottom-0">
- <label class="label-light col-sm-3">
- {{ formLabelName }}
+ <label class="label-light col-form-label col-sm-3 ide-new-modal-label">
+ {{ __('Name') }}
</label>
<div class="col-sm-9">
<input
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 14673754503..c5092d8e04d 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,64 +1,63 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
-import commitFilesList from './commit_sidebar/list.vue';
-import CommitMessageField from './commit_sidebar/message_field.vue';
+import CommitFilesList from './commit_sidebar/list.vue';
+import EmptyState from './commit_sidebar/empty_state.vue';
import * as consts from '../stores/modules/commit/constants';
-import Actions from './commit_sidebar/actions.vue';
+import { activityBarViews } from '../constants';
export default {
components: {
DeprecatedModal,
- icon,
- commitFilesList,
- Actions,
- LoadingButton,
- CommitMessageField,
+ Icon,
+ CommitFilesList,
+ EmptyState,
},
directives: {
tooltip,
},
- props: {
- noChangesStateSvgPath: {
- type: String,
- required: true,
- },
- committedStateSvgPath: {
- type: String,
- required: true,
- },
- },
computed: {
...mapState([
- 'currentProjectId',
- 'currentBranchId',
+ 'changedFiles',
+ 'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
- 'changedFiles',
+ 'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
- ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
- statusSvg() {
- return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
+ ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']),
+ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
+ showStageUnstageArea() {
+ return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
},
- methods: {
- ...mapActions(['setPanelCollapsedStatus']),
- ...mapActions('commit', [
- 'updateCommitMessage',
- 'discardDraft',
- 'commitChanges',
- 'updateCommitAction',
- ]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
+ watch: {
+ hasChanges() {
+ if (!this.hasChanges) {
+ this.updateActivityBarView(activityBarViews.edit);
+ }
},
+ },
+ mounted() {
+ if (this.lastOpenedFile) {
+ this.openPendingTab({
+ file: this.lastOpenedFile,
+ })
+ .then(changeViewer => {
+ if (changeViewer) {
+ this.updateViewer('diff');
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
+ },
+ methods: {
+ ...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
+ ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
@@ -69,9 +68,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
- :class="{
- 'multi-file-commit-empty-state-container': !changedFiles.length
- }"
>
<deprecated-modal
id="ide-create-branch-modal"
@@ -85,76 +81,30 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
- <commit-files-list
- title="Staged"
- :file-list="changedFiles"
- :collapsed="rightPanelCollapsed"
- @toggleCollapsed="toggleCollapsed"
- />
<template
- v-if="changedFiles.length"
+ v-if="showStageUnstageArea"
>
- <form
- class="form-horizontal multi-file-commit-form"
- @submit.prevent.stop="commitChanges"
- v-if="!rightPanelCollapsed"
- >
- <commit-message-field
- :text="commitMessage"
- @input="updateCommitMessage"
- />
- <div class="clearfix prepend-top-15">
- <actions />
- <loading-button
- :loading="submitCommitLoading"
- :disabled="commitButtonDisabled"
- container-class="btn btn-success btn-sm pull-left"
- :label="__('Commit')"
- @click="commitChanges"
- />
- <button
- v-if="!discardDraftButtonDisabled"
- type="button"
- class="btn btn-default btn-sm pull-right"
- @click="discardDraft"
- >
- {{ __('Discard draft') }}
- </button>
- </div>
- </form>
+ <commit-files-list
+ class="is-first"
+ icon-name="unstaged"
+ :title="__('Unstaged')"
+ :file-list="changedFiles"
+ action="stageAllChanges"
+ :action-btn-text="__('Stage all')"
+ item-action-component="stage-button"
+ />
+ <commit-files-list
+ icon-name="staged"
+ :title="__('Staged')"
+ :file-list="stagedFiles"
+ action="unstageAllChanges"
+ :action-btn-text="__('Unstage all')"
+ item-action-component="unstage-button"
+ :staged-list="true"
+ />
</template>
- <div
- v-else-if="!rightPanelCollapsed"
- class="row js-empty-state"
- >
- <div class="col-xs-10 col-xs-offset-1">
- <div class="svg-content svg-80">
- <img :src="statusSvg" />
- </div>
- </div>
- <div class="col-xs-10 col-xs-offset-1">
- <div
- class="text-content text-center"
- v-if="!lastCommitMsg"
- >
- <h4>
- {{ __('No changes') }}
- </h4>
- <p>
- {{ __('Edit files in the editor and commit changes here') }}
- </p>
- </div>
- <div
- class="text-content text-center"
- v-else
- >
- <h4>
- {{ __('All changes are committed') }}
- </h4>
- <p v-html="lastCommitMsg">
- </p>
- </div>
- </div>
- </div>
+ <empty-state
+ v-if="unusedSeal"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 711bafa17a9..a281ecb95c8 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -3,6 +3,7 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue';
@@ -19,8 +20,14 @@ export default {
},
},
computed: {
- ...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
- ...mapGetters(['currentMergeRequest']),
+ ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']),
+ ...mapGetters([
+ 'currentMergeRequest',
+ 'getStagedFile',
+ 'isEditModeActive',
+ 'isCommitModeActive',
+ 'isReviewModeActive',
+ ]),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -36,10 +43,29 @@ export default {
},
},
watch: {
- file(oldVal, newVal) {
+ file(newVal, oldVal) {
+ if (oldVal.pending) {
+ this.removePendingTab(oldVal);
+ }
+
// Compare key to allow for files opened in review mode to be cached differently
- if (newVal.key !== this.file.key) {
+ if (oldVal.key !== this.file.key) {
this.initMonaco();
+
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
+ }
+ }
+ },
+ currentActivityView() {
+ if (this.currentActivityView !== activityBarViews.edit) {
+ this.setFileViewMode({
+ file: this.file,
+ viewMode: 'edit',
+ });
}
},
rightPanelCollapsed() {
@@ -77,7 +103,7 @@ export default {
'setFileViewMode',
'setFileEOL',
'updateViewer',
- 'updateDelayViewerUpdated',
+ 'removePendingTab',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@@ -89,14 +115,6 @@ export default {
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
- const viewerPromise = this.delayViewerUpdated
- ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
- : Promise.resolve();
-
- return viewerPromise;
- })
- .then(() => {
- this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch(err => {
@@ -108,10 +126,10 @@ export default {
this.editor.dispose();
this.$nextTick(() => {
- if (this.viewer === 'editor') {
+ if (this.viewer === viewerTypes.edit) {
this.editor.createInstance(this.$refs.editor);
} else {
- this.editor.createDiffInstance(this.$refs.editor);
+ this.editor.createDiffInstance(this.$refs.editor, !this.isReviewModeActive);
}
this.setupEditor();
@@ -120,9 +138,14 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
- this.model = this.editor.createModel(this.file);
+ const head = this.getStagedFile(this.file.path);
+
+ this.model = this.editor.createModel(
+ this.file,
+ this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
+ );
- if (this.viewer === 'mrdiff') {
+ if (this.viewer === viewerTypes.mr && this.file.mrChange) {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
@@ -163,6 +186,7 @@ export default {
});
},
},
+ viewerTypes,
};
</script>
@@ -171,16 +195,17 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
- <div class="ide-mode-tabs clearfix">
+ <div class="ide-mode-tabs clearfix" >
<ul
- class="nav-links pull-left"
- v-if="!shouldHideEditor">
+ class="nav-links float-left"
+ v-if="!shouldHideEditor && isEditModeActive"
+ >
<li :class="editTabCSS">
<a
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'edit' })">
- <template v-if="viewer === 'editor'">
+ <template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
@@ -207,6 +232,9 @@ export default {
v-show="!shouldHideEditor && file.viewMode === 'edit'"
ref="editor"
class="multi-file-editor-holder"
+ :class="{
+ 'is-readonly': isCommitModeActive,
+ }"
>
</div>
<content-viewer
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 3b5068d4910..442697e1c80 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -1,22 +1,29 @@
<script>
-import { mapActions } from 'vuex';
-import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import fileIcon from '~/vue_shared/components/file_icon.vue';
+import { mapActions, mapGetters } from 'vuex';
+import { n__, __, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import router from '../ide_router';
-import newDropdown from './new_dropdown/index.vue';
-import fileStatusIcon from './repo_file_status_icon.vue';
-import changedFileIcon from './changed_file_icon.vue';
-import mrFileIcon from './mr_file_icon.vue';
+import NewDropdown from './new_dropdown/index.vue';
+import FileStatusIcon from './repo_file_status_icon.vue';
+import ChangedFileIcon from './changed_file_icon.vue';
+import MrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
+ directives: {
+ tooltip,
+ },
components: {
- skeletonLoadingContainer,
- newDropdown,
- fileStatusIcon,
- fileIcon,
- changedFileIcon,
- mrFileIcon,
+ SkeletonLoadingContainer,
+ NewDropdown,
+ FileStatusIcon,
+ FileIcon,
+ ChangedFileIcon,
+ MrFileIcon,
+ Icon,
},
props: {
file: {
@@ -27,8 +34,41 @@ export default {
type: Number,
required: true,
},
+ disableActionDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
+ ...mapGetters([
+ 'getChangesInFolder',
+ 'getUnstagedFilesCountForPath',
+ 'getStagedFilesCountForPath',
+ ]),
+ folderUnstagedCount() {
+ return this.getUnstagedFilesCountForPath(this.file.path);
+ },
+ folderStagedCount() {
+ return this.getStagedFilesCountForPath(this.file.path);
+ },
+ changesCount() {
+ return this.getChangesInFolder(this.file.path);
+ },
+ folderChangesTooltip() {
+ if (this.changesCount === 0) return undefined;
+
+ if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) {
+ return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount);
+ } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) {
+ return n__('%d staged change', '%d staged changes', this.folderStagedCount);
+ }
+
+ return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
+ unstaged: this.folderUnstagedCount,
+ staged: this.folderStagedCount,
+ });
+ },
isTree() {
return this.file.type === 'tree';
},
@@ -48,23 +88,30 @@ export default {
'is-open': this.file.opened,
};
},
+ showTreeChangesCount() {
+ return this.isTree && this.changesCount > 0 && !this.file.opened;
+ },
+ showChangedFileIcon() {
+ return this.file.changed || this.file.tempFile || this.file.staged;
+ },
},
updated() {
if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView();
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ });
}
},
methods: {
- ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
+ ...mapActions(['toggleTreeOpen']),
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
- return this.updateDelayViewerUpdated(true).then(() => {
- router.push(`/project${this.file.url}`);
- });
+ router.push(`/project${this.file.url}`);
},
},
};
@@ -75,11 +122,11 @@ export default {
<div
class="file"
:class="fileClass"
+ @click="clickFile"
+ role="button"
>
<div
class="file-name"
- @click="clickFile"
- role="button"
>
<span
class="ide-file-name str-truncated"
@@ -97,21 +144,40 @@ export default {
:file="file"
/>
</span>
- <span class="pull-right">
+ <span class="float-right ide-file-icon-holder">
<mr-file-icon
v-if="file.mrChange"
/>
+ <span
+ v-if="showTreeChangesCount"
+ class="ide-tree-changes"
+ >
+ {{ changesCount }}
+ <icon
+ v-tooltip
+ :title="folderChangesTooltip"
+ data-container="body"
+ data-placement="right"
+ name="file-modified"
+ :size="12"
+ css-classes="prepend-left-5 multi-file-modified"
+ />
+ </span>
<changed-file-icon
+ v-else-if="showChangedFileIcon"
:file="file"
- v-if="file.changed || file.tempFile"
+ :show-tooltip="true"
+ :show-staged-icon="true"
+ :force-modified-icon="true"
+ class="pull-right"
/>
</span>
<new-dropdown
- v-if="isTree"
+ v-if="isTree && !disableActionDropdown"
:project-id="file.projectId"
:branch="file.branchId"
:path="file.path"
- class="pull-right prepend-left-8"
+ class="float-right prepend-left-8"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
index 79af8c0b0c7..3e47da88050 100644
--- a/app/assets/javascripts/ide/components/repo_loading_file.vue
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -25,13 +25,13 @@
/>
</td>
<template v-if="!leftPanelCollapsed">
- <td class="hidden-sm hidden-xs">
+ <td class="d-none d-sm-none d-md-block">
<skeleton-loading-container
:small="true"
/>
</td>
- <td class="hidden-xs">
+ <td class="d-none d-sm-block">
<skeleton-loading-container
class="animation-container-right"
:small="true"
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 304a73ed1ad..fb26b973236 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -26,13 +26,18 @@ export default {
},
computed: {
closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
+ if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
- return this.tab.changed ? !this.tabMouseOver : false;
+ if (this.tab.pending) return true;
+
+ return this.fileHasChanged ? !this.tabMouseOver : false;
+ },
+ fileHasChanged() {
+ return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
@@ -42,18 +47,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
- this.openPendingTab(tab);
+ this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
- if (this.tab.changed) {
+ if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},
@@ -63,15 +68,32 @@ export default {
<template>
<li
+ :class="{
+ active: tab.active
+ }"
@click="clickFile(tab)"
@mouseover="mouseOverTab"
@mouseout="mouseOutTab"
>
+ <div
+ class="multi-file-tab"
+ :title="tab.url"
+ >
+ <file-icon
+ :file-name="tab.name"
+ :size="16"
+ />
+ {{ tab.name }}
+ <file-status-icon
+ :file="tab"
+ />
+ </div>
<button
type="button"
class="multi-file-tab-close"
@click.stop.prevent="closeFile(tab)"
:aria-label="closeLabel"
+ :disabled="tab.pending"
>
<icon
v-if="!showChangedIcon"
@@ -81,24 +103,8 @@ export default {
<changed-file-icon
v-else
:file="tab"
+ :force-modified-icon="true"
/>
</button>
-
- <div
- class="multi-file-tab"
- :class="{
- active: tab.active
- }"
- :title="tab.url"
- >
- <file-icon
- :file-name="tab.name"
- :size="16"
- />
- {{ tab.name }}
- <file-status-icon
- :file="tab"
- />
- </div>
</li>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 7bd646ba9b0..99e51097e12 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -32,16 +32,6 @@ export default {
default: '',
},
},
- data() {
- return {
- showShadow: false,
- };
- },
- updated() {
- if (!this.$refs.tabsScroller) return;
-
- this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
- },
methods: {
...mapActions(['updateViewer', 'removePendingTab']),
openFileViewer(viewer) {
@@ -71,12 +61,5 @@ export default {
:tab="tab"
/>
</ul>
- <editor-mode
- :viewer="viewer"
- :show-shadow="showShadow"
- :has-changes="hasChanges"
- :merge-request-id="mergeRequestId"
- @click="openFileViewer"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index b60d042e0be..83fe22f40a4 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -1,3 +1,22 @@
// Fuzzy file finder
+export const MAX_FILE_FINDER_RESULTS = 40;
+export const FILE_FINDER_ROW_HEIGHT = 55;
+export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
+
+export const MAX_WINDOW_HEIGHT_COMPACT = 750;
+
+// Commit message textarea
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
+
+export const activityBarViews = {
+ edit: 'ide-tree',
+ commit: 'commit-section',
+ review: 'ide-review',
+};
+
+export const viewerTypes = {
+ mr: 'mrdiff',
+ edit: 'editor',
+ diff: 'diff',
+};
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 4a0a303d5a6..a21cec4e8d8 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueRouter from 'vue-router';
import flash from '~/flash';
import store from './stores';
+import { activityBarViews } from './constants';
Vue.use(VueRouter);
@@ -40,7 +41,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
children: [
{
- path: ':targetmode(edit|tree|blob)/:branch/*',
+ path: ':targetmode(edit|tree|blob)/*',
component: EmptyRouterComponent,
},
{
@@ -62,21 +63,27 @@ router.beforeEach((to, from, next) => {
.then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
- if (to.params.branch) {
+ const baseSplit = to.params[0].split('/-/');
+ const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0];
+
+ if (branchId) {
+ const basePath = baseSplit.length > 1 ? baseSplit[1] : '';
+
+ store.dispatch('setCurrentBranchId', branchId);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
- branchId: to.params.branch,
+ branchId,
});
store
.dispatch('getFiles', {
projectId: fullProjectId,
- branchId: to.params.branch,
+ branchId,
})
.then(() => {
- if (to.params[0]) {
- const path =
- to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(store.state.entries).find(
key => key === path && !store.state.entries[key].pending,
);
@@ -99,14 +106,14 @@ router.beforeEach((to, from, next) => {
throw e;
});
} else if (to.params.mrid) {
- store.dispatch('updateViewer', 'mrdiff');
-
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
+ store.dispatch('updateActivityBarView', activityBarViews.review);
+
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index cbfb3dc54f2..c5835cd3b06 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -4,7 +4,9 @@ import ide from './components/ide.vue';
import store from './stores';
import router from './ide_router';
-function initIde(el) {
+Vue.use(Translate);
+
+export function initIde(el) {
if (!el) return null;
return new Vue({
@@ -14,20 +16,25 @@ function initIde(el) {
components: {
ide,
},
- render(createElement) {
- return createElement('ide', {
- props: {
- emptyStateSvgPath: el.dataset.emptyStateSvgPath,
- noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
- committedStateSvgPath: el.dataset.committedStateSvgPath,
- },
+ created() {
+ this.$store.dispatch('setEmptyStateSvgs', {
+ emptyStateSvgPath: el.dataset.emptyStateSvgPath,
+ noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
+ committedStateSvgPath: el.dataset.committedStateSvgPath,
});
},
+ render(createElement) {
+ return createElement('ide');
+ },
});
}
-const ideElement = document.getElementById('ide');
-
-Vue.use(Translate);
-
-initIde(ideElement);
+// tell webpack to load assets from origin so that web workers don't break
+export function resetServiceWorkersPublicPath() {
+ // __webpack_public_path__ is a global variable that can be used to adjust
+ // the webpack publicPath setting at runtime.
+ // see: https://webpack.js.org/guides/public-path/
+ const relativeRootPath = (gon && gon.relative_url_root) || '';
+ const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+}
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e47adae99ed..b1e43a1e38c 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -3,22 +3,23 @@ import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
- constructor(monaco, file) {
+ constructor(monaco, file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
+ this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
- this.file.raw,
+ head ? head.content : this.file.raw,
undefined,
- new this.monaco.Uri(null, null, `original/${this.file.key}`),
+ new this.monaco.Uri(null, null, `original/${this.path}`),
)),
(this.model = this.monaco.editor.createModel(
this.content,
undefined,
- new this.monaco.Uri(null, null, this.file.key),
+ new this.monaco.Uri(null, null, this.path),
)),
);
if (this.file.mrChange) {
@@ -26,18 +27,20 @@ export default class Model {
(this.baseModel = this.monaco.editor.createModel(
this.file.baseRaw,
undefined,
- new this.monaco.Uri(null, null, `target/${this.file.path}`),
+ new this.monaco.Uri(null, null, `target/${this.path}`),
)),
);
}
- this.events = new Map();
+ this.events = new Set();
this.updateContent = this.updateContent.bind(this);
+ this.updateNewContent = this.updateNewContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
get url() {
@@ -73,22 +76,36 @@ export default class Model {
}
onChange(cb) {
- this.events.set(
- this.path,
- this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
- );
+ this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
+ }
+
+ onDispose(cb) {
+ this.events.add(cb);
}
- updateContent(content) {
+ updateContent({ content, changed }) {
this.getOriginalModel().setValue(content);
+
+ if (!changed) {
+ this.getModel().setValue(content);
+ }
+ }
+
+ updateNewContent(content) {
this.getModel().setValue(content);
}
dispose() {
this.disposable.dispose();
+
+ this.events.forEach(cb => {
+ if (typeof cb === 'function') cb();
+ });
+
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
- eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
+ eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
+ eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 0e7b563b5d6..7f643969480 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -17,12 +17,12 @@ export default class ModelManager {
return this.models.get(key);
}
- addModel(file) {
+ addModel(file, head = null) {
if (this.hasCachedModel(file.key)) {
return this.getModel(file.key);
}
- const model = new Model(this.monaco, file);
+ const model = new Model(this.monaco, file, head);
this.models.set(model.path, model);
this.disposable.add(model);
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 42904774747..13d477bb2cf 100644
--- a/app/assets/javascripts/ide/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
@@ -38,6 +38,15 @@ export default class DecorationsController {
);
}
+ hasDecorations(model) {
+ return this.decorations.has(model.url);
+ }
+
+ removeDecorations(model) {
+ this.decorations.delete(model.url);
+ this.editorDecorations.delete(model.url);
+ }
+
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index b136545ad11..f579424cf33 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
-export const getDiffChangeType = (change) => {
+export const getDiffChangeType = change => {
if (change.modified) {
return 'modified';
} else if (change.added) {
@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
};
export const getDecorator = change => ({
- range: new monaco.Range(
- change.lineNumber,
- 1,
- change.endLineNumber,
- 1,
- ),
+ range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
@@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
+ this.models = new Map();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
@@ -42,7 +38,15 @@ export default class DirtyDiffController {
}
attachModel(model) {
+ if (this.models.has(model.url)) return;
+
model.onChange(() => this.throttledComputeDiff(model));
+ model.onDispose(() => {
+ this.decorationsController.removeDecorations(model);
+ this.models.delete(model.url);
+ });
+
+ this.models.set(model.url, model);
}
computeDiff(model) {
@@ -54,7 +58,11 @@ export default class DirtyDiffController {
}
reDecorate(model) {
- this.decorationsController.decorate(model);
+ if (this.decorationsController.hasDecorations(model)) {
+ this.decorationsController.decorate(model);
+ } else {
+ this.computeDiff(model);
+ }
}
decorate({ data }) {
@@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() {
this.disposable.dispose();
+ this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 001737d6ee8..9c3bb9cc17d 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,10 +1,12 @@
import _ from 'underscore';
+import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
+import keymap from './keymap.json';
export const clearDomElement = el => {
if (!el || !el.firstChild) return;
@@ -53,32 +55,36 @@ export default class Editor {
)),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
- createDiffInstance(domElement) {
+ createDiffInstance(domElement, readOnly = true) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
- readOnly: true,
quickSuggestions: false,
occurrencesHighlight: false,
- renderLineHighlight: 'none',
- hideCursorInOverviewRuler: true,
renderSideBySide: Editor.renderSideBySide(domElement),
+ readOnly,
+ renderLineHighlight: readOnly ? 'all' : 'none',
+ hideCursorInOverviewRuler: !readOnly,
})),
);
+ this.addCommands();
+
window.addEventListener('resize', this.debouncedUpdate, false);
}
}
- createModel(file) {
- return this.modelManager.addModel(file);
+ createModel(file, head = null) {
+ return this.modelManager.addModel(file, head);
}
attachModel(model) {
@@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700;
}
+
+ addCommands() {
+ const getKeyCode = key => {
+ const monacoKeyMod = key.indexOf('KEY_') === 0;
+
+ return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
+ };
+
+ keymap.forEach(command => {
+ const keybindings = command.bindings.map(binding => {
+ const keys = binding.split('+');
+
+ // eslint-disable-next-line no-bitwise
+ return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
+ });
+
+ this.instance.addAction({
+ id: command.id,
+ label: command.label,
+ keybindings,
+ run() {
+ store.dispatch(command.action.name, command.action.params);
+ return null;
+ },
+ });
+ });
+ }
}
diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json
new file mode 100644
index 00000000000..131abfebbed
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/keymap.json
@@ -0,0 +1,11 @@
+[
+ {
+ "id": "file-finder",
+ "label": "File finder",
+ "bindings": ["CtrlCmd+KEY_P"],
+ "action": {
+ "name": "toggleFileFinder",
+ "params": true
+ }
+ }
+]
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index a12e637616a..e8b51f2b516 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -75,4 +75,8 @@ export default {
},
});
},
+ lastCommitPipelines({ getters }) {
+ const commitSha = getters.lastCommit.id;
+ return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
+ },
};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index c6ba679d99c..1a98b42761e 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,3 +1,4 @@
+import $ from 'jquery';
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@@ -32,6 +33,19 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
+export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
+ if (e) {
+ $(e.currentTarget)
+ .tooltip('hide')
+ .blur();
+ }
+
+ dispatch('setPanelCollapsedStatus', {
+ side: 'right',
+ collapsed: !state.rightPanelCollapsed,
+ });
+};
+
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
@@ -60,7 +74,7 @@ export const createTempEntry = (
}
worker.addEventListener('message', ({ data }) => {
- const { file } = data;
+ const { file, parentPath } = data;
worker.terminate();
@@ -76,6 +90,10 @@ export const createTempEntry = (
dispatch('setFileActive', file.path);
}
+ if (parentPath && !state.entries[parentPath].opened) {
+ commit(types.TOGGLE_TREE_OPEN, parentPath);
+ }
+
resolve(file);
});
@@ -104,6 +122,16 @@ export const scrollToTab = () => {
});
};
+export const stageAllChanges = ({ state, commit }) => {
+ commit(types.SET_LAST_COMMIT_MSG, '');
+
+ state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+};
+
+export const unstageAllChanges = ({ state, commit }) => {
+ state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+};
+
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
@@ -112,7 +140,39 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
+export const updateActivityBarView = ({ commit }, view) => {
+ commit(types.UPDATE_ACTIVITY_BAR_VIEW, view);
+};
+
+export const setEmptyStateSvgs = ({ commit }, svgs) => {
+ commit(types.SET_EMPTY_STATE_SVGS, svgs);
+};
+
+export const setCurrentBranchId = ({ commit }, currentBranchId) => {
+ commit(types.SET_CURRENT_BRANCH, currentBranchId);
+};
+
+export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
+ commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
+
+ if (file.parentPath) {
+ dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
+ }
+};
+
+export const toggleFileFinder = ({ commit }, fileFindVisible) =>
+ commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
+
+export const burstUnusedSeal = ({ state, commit }) => {
+ if (state.unusedSeal) {
+ commit(types.BURST_UNUSED_SEAL);
+ }
+};
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 1a17320a1ea..13aea91d8ba 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,6 +5,7 @@ import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
import { setPageTitle } from '../utils';
+import { viewerTypes } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
const path = file.path;
@@ -23,10 +24,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
const nextFileToOpen = state.openFiles[nextIndexToOpen];
if (nextFileToOpen.pending) {
- dispatch('updateViewer', 'diff');
- dispatch('openPendingTab', nextFileToOpen);
+ dispatch('updateViewer', viewerTypes.diff);
+ dispatch('openPendingTab', {
+ file: nextFileToOpen,
+ keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
+ });
} else {
- dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
}
} else if (!state.openFiles.length) {
@@ -60,7 +63,9 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
- .getFileData(file.url)
+ .getFileData(
+ `${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
+ )
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
@@ -114,7 +119,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
});
};
-export const changeFileContent = ({ state, commit }, { path, content }) => {
+export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content });
@@ -125,6 +130,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => {
} else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
+
+ dispatch('burstUnusedSeal', {}, { root: true });
};
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
@@ -153,7 +160,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
-export const discardFileChanges = ({ state, commit }, path) => {
+export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path];
commit(types.DISCARD_FILE_CHANGES, path);
@@ -161,17 +168,41 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
+ } else if (getters.activeFile && file.path === getters.activeFile.path) {
+ dispatch('updateDelayViewerUpdated', true)
+ .then(() => {
+ router.push(`/project${file.url}`);
+ })
+ .catch(e => {
+ throw e;
+ });
}
- eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
+ eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content);
+ eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
};
-export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
- if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
- return false;
+export const stageChange = ({ commit, state }, path) => {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ commit(types.STAGE_CHANGE, path);
+ commit(types.SET_LAST_COMMIT_MSG, '');
+
+ if (stagedFile) {
+ eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
+};
+
+export const unstageChange = ({ commit }, path) => {
+ commit(types.UNSTAGE_CHANGE, path);
+};
+
+export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
+ if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
+
+ state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
- commit(types.ADD_PENDING_TAB, { file });
+ commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 4eb23b2ee0f..cece9154c82 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,6 +1,11 @@
+import Visibility from 'visibilityjs';
import flash from '~/flash';
+import { __ } from '~/locale';
import service from '../../services';
import * as types from '../mutation_types';
+import Poll from '../../../lib/utils/poll';
+
+let eTagPoll;
export const getProjectData = (
{ commit, state, dispatch },
@@ -21,7 +26,7 @@ export const getProjectData = (
})
.catch(() => {
flash(
- 'Error loading project data. Please try again.',
+ __('Error loading project data. Please try again.'),
'alert',
document,
null,
@@ -55,12 +60,11 @@ export const getBranchData = (
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
flash(
- 'Error loading branch data. Please try again.',
+ __('Error loading branch data. Please try again.'),
'alert',
document,
null,
@@ -73,3 +77,75 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
+
+export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) =>
+ service
+ .getBranchData(projectId, branchId)
+ .then(({ data }) => {
+ commit(types.SET_BRANCH_COMMIT, {
+ projectId,
+ branchId,
+ commit: data.commit,
+ });
+ })
+ .catch(() => {
+ flash(__('Error loading last commit.'), 'alert', document, null, false, true);
+ });
+
+export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => {
+ if (data.pipelines && data.pipelines.length) {
+ const lastCommitHash =
+ state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id;
+ const lastCommitPipeline = data.pipelines.find(
+ pipeline => pipeline.commit.id === lastCommitHash,
+ );
+ commit(types.SET_LAST_COMMIT_PIPELINE, {
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ pipeline: lastCommitPipeline || {},
+ });
+ }
+
+ return data;
+};
+
+export const pipelinePoll = ({ getters, dispatch }) => {
+ eTagPoll = new Poll({
+ resource: service,
+ method: 'lastCommitPipelines',
+ data: {
+ getters,
+ },
+ successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }),
+ errorCallback: () => {
+ flash(
+ __('Something went wrong while fetching the latest pipeline status.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ eTagPoll.restart();
+ } else {
+ eTagPoll.stop();
+ }
+ });
+};
+
+export const stopPipelinePolling = () => {
+ eTagPoll.stop();
+};
+
+export const restartPipelinePolling = () => {
+ eTagPoll.restart();
+};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index a77cdbc13c8..b239a605371 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,3 +1,6 @@
+import { getChangesCountForFiles, filePathMatches } from './utils';
+import { activityBarViews } from '../constants';
+
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
@@ -28,10 +31,61 @@ export const currentMergeRequest = state => {
return null;
};
-// eslint-disable-next-line no-confusing-arrow
-export const currentIcon = state =>
- state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
+export const currentProject = state => state.projects[state.currentProjectId];
+
+export const currentTree = state =>
+ state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
-export const hasChanges = state => !!state.changedFiles.length;
+export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId;
+
+export const allBlobs = state =>
+ Object.keys(state.entries)
+ .reduce((acc, key) => {
+ const entry = state.entries[key];
+
+ if (entry.type === 'blob') {
+ acc.push(entry);
+ }
+
+ return acc;
+ }, [])
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+
+export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path);
+export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
+
+export const lastOpenedFile = state =>
+ [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0];
+
+export const isEditModeActive = state => state.currentActivityView === activityBarViews.edit;
+export const isCommitModeActive = state => state.currentActivityView === activityBarViews.commit;
+export const isReviewModeActive = state => state.currentActivityView === activityBarViews.review;
+
+export const someUncommitedChanges = state =>
+ !!(state.changedFiles.length || state.stagedFiles.length);
+
+export const getChangesInFolder = state => path => {
+ const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f, path)).length;
+ const stagedFilesCount = state.stagedFiles.filter(
+ f => filePathMatches(f, path) && !getChangedFile(state)(f.path),
+ ).length;
+
+ return changedFilesCount + stagedFilesCount;
+};
+
+export const getUnstagedFilesCountForPath = state => path =>
+ getChangesCountForFiles(state.changedFiles, path);
+
+export const getStagedFilesCountForPath = state => path =>
+ getChangesCountForFiles(state.stagedFiles, path);
+
+export const lastCommit = (state, getters) => {
+ const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+
+ return branch ? branch.commit : null;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 7c82ce7976b..699710055e3 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -5,6 +5,7 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import commitModule from './modules/commit';
+import pipelines from './modules/pipelines';
Vue.use(Vuex);
@@ -15,5 +16,6 @@ export default new Vuex.Store({
getters,
modules: {
commit: commitModule,
+ pipelines,
},
});
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 367c45f7e2d..cd25c3060f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -8,6 +8,7 @@ import router from '../../../ide_router';
import service from '../../../services';
import * as types from './mutation_types';
import * as consts from './constants';
+import { activityBarViews } from '../../../constants';
import eventHub from '../../../eventhub';
export const updateCommitMessage = ({ commit }, message) => {
@@ -75,7 +76,7 @@ export const checkCommitStatus = ({ rootState }) =>
export const updateFilesAfterCommit = (
{ commit, dispatch, state, rootState, rootGetters },
- { data, branch },
+ { data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = {
@@ -98,23 +99,14 @@ export const updateFilesAfterCommit = (
{ root: true },
);
- rootState.changedFiles.forEach(entry => {
- commit(
- rootTypes.SET_LAST_COMMIT_DATA,
- {
- entry,
- lastCommit,
- },
- { root: true },
- );
-
- eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
+ rootState.stagedFiles.forEach(file => {
+ const changedFile = rootState.changedFiles.find(f => f.path === file.path);
commit(
- rootTypes.SET_FILE_RAW_DATA,
+ rootTypes.UPDATE_FILE_AFTER_COMMIT,
{
- file: entry,
- raw: entry.content,
+ file,
+ lastCommit,
},
{ root: true },
);
@@ -122,23 +114,22 @@ export const updateFilesAfterCommit = (
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
- file: entry,
+ file,
changed: false,
},
{ root: true },
);
- });
- commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
+ dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
- if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
- router.push(
- `/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
- );
- }
+ eventHub.$emit(`editor.update.model.content.${file.key}`, {
+ content: file.content,
+ changed: !!changedFile,
+ });
+ });
};
-export const commitChanges = ({ commit, state, getters, dispatch, rootState }) => {
+export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => {
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
const payload = createCommitPayload(getters.branchName, newBranch, state, rootState);
const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus');
@@ -184,8 +175,52 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
{ root: true },
);
}
+
+ commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
+
+ setTimeout(() => {
+ commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
+ }, 5000);
})
- .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
+ .then(() => {
+ if (rootGetters.lastOpenedFile) {
+ dispatch(
+ 'openPendingTab',
+ {
+ file: rootGetters.lastOpenedFile,
+ },
+ { root: true },
+ )
+ .then(changeViewer => {
+ if (changeViewer) {
+ dispatch('updateViewer', 'diff', { root: true });
+ }
+ })
+ .catch(e => {
+ throw e;
+ });
+ } else {
+ dispatch('updateActivityBarView', activityBarViews.edit, { root: true });
+ dispatch('updateViewer', 'editor', { root: true });
+
+ router.push(
+ `/project/${rootState.currentProjectId}/blob/${getters.branchName}/-/${
+ rootGetters.activeFile.path
+ }`,
+ );
+ }
+ })
+ .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
+ .then(() =>
+ dispatch(
+ 'refreshLastCommitData',
+ {
+ projectId: rootState.currentProjectId,
+ branchId: rootState.currentBranchId,
+ },
+ { root: true },
+ ),
+ );
})
.catch(err => {
let errMsg = __('Error committing changes. Please try again.');
@@ -198,3 +233,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(types.UPDATE_LOADING, false);
});
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
index f7cdd6adb0c..d01060201f2 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -1,12 +1,17 @@
import * as consts from './constants';
-export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
+const BRANCH_SUFFIX_COUNT = 5;
+
+export const discardDraftButtonDisabled = state =>
+ state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
- getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
+ getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) =>
- `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
+ `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
+ -BRANCH_SUFFIX_COUNT,
+ )}`;
export const branchName = (state, getters, rootState) => {
if (
@@ -22,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
new file mode 100644
index 00000000000..07f7b201f2e
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -0,0 +1,49 @@
+import { __ } from '../../../../locale';
+import Api from '../../../../api';
+import flash from '../../../../flash';
+import * as types from './mutation_types';
+
+export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
+export const receiveLatestPipelineError = ({ commit }) => {
+ flash(__('There was an error loading latest pipeline'));
+ commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
+};
+export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
+
+export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
+ dispatch('requestLatestPipeline');
+
+ return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' })
+ .then(({ data }) => {
+ dispatch('receiveLatestPipelineSuccess', data.pop());
+ })
+ .catch(() => dispatch('receiveLatestPipelineError'));
+};
+
+export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS);
+export const receiveJobsError = ({ commit }) => {
+ flash(__('There was an error loading jobs'));
+ commit(types.RECEIVE_JOBS_ERROR);
+};
+export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data);
+
+export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => {
+ dispatch('requestJobs');
+
+ Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
+ page,
+ })
+ .then(({ data, headers }) => {
+ const nextPage = headers && headers['x-next-page'];
+
+ dispatch('receiveJobsSuccess', data);
+
+ if (nextPage) {
+ dispatch('fetchJobs', nextPage);
+ }
+ })
+ .catch(() => dispatch('receiveJobsError'));
+};
+
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
new file mode 100644
index 00000000000..d6c91f5b64d
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -0,0 +1,7 @@
+export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
+
+export const failedJobs = state =>
+ state.stages.reduce(
+ (acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
+ [],
+ );
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
new file mode 100644
index 00000000000..b44c3141b81
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/index.js
@@ -0,0 +1,12 @@
+import state from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+import * as getters from './getters';
+
+export default {
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+ getters,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
new file mode 100644
index 00000000000..6b5701670a6
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js
@@ -0,0 +1,7 @@
+export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE';
+export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR';
+export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS';
+
+export const REQUEST_JOBS = 'REQUEST_JOBS';
+export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
+export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
new file mode 100644
index 00000000000..2b16e57b386
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js
@@ -0,0 +1,53 @@
+/* eslint-disable no-param-reassign */
+import * as types from './mutation_types';
+
+export default {
+ [types.REQUEST_LATEST_PIPELINE](state) {
+ state.isLoadingPipeline = true;
+ },
+ [types.RECEIVE_LASTEST_PIPELINE_ERROR](state) {
+ state.isLoadingPipeline = false;
+ },
+ [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) {
+ state.isLoadingPipeline = false;
+
+ if (pipeline) {
+ state.latestPipeline = {
+ id: pipeline.id,
+ status: pipeline.status,
+ };
+ }
+ },
+ [types.REQUEST_JOBS](state) {
+ state.isLoadingJobs = true;
+ },
+ [types.RECEIVE_JOBS_ERROR](state) {
+ state.isLoadingJobs = false;
+ },
+ [types.RECEIVE_JOBS_SUCCESS](state, jobs) {
+ state.isLoadingJobs = false;
+
+ state.stages = jobs.reduce((acc, job) => {
+ let stage = acc.find(s => s.title === job.stage);
+
+ if (!stage) {
+ stage = {
+ title: job.stage,
+ jobs: [],
+ };
+
+ acc.push(stage);
+ }
+
+ stage.jobs = stage.jobs.concat({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ stage: job.stage,
+ duration: job.duration,
+ });
+
+ return acc;
+ }, state.stages);
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
new file mode 100644
index 00000000000..6f22542aaea
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js
@@ -0,0 +1,6 @@
+export default () => ({
+ isLoadingPipeline: false,
+ isLoadingJobs: false,
+ latestPipeline: null,
+ stages: [],
+});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e3f504e5ab0..0a3f8d031c4 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -5,6 +5,7 @@ export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
+export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';
@@ -19,8 +20,10 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
+export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
@@ -51,5 +54,15 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
+export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
+export const STAGE_CHANGE = 'STAGE_CHANGE';
+export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
+
+export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
+
+export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
+export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
+export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
+export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 5e5eb831662..a257e2ef025 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
+import { sortTree } from './utils';
export default {
[types.SET_INITIAL_DATA](state, data) {
@@ -49,6 +50,11 @@ export default {
lastCommitMsg,
});
},
+ [types.CLEAR_STAGED_CHANGES](state) {
+ Object.assign(state, {
+ stagedFiles: [],
+ });
+ },
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@@ -68,7 +74,7 @@ export default {
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
);
Object.assign(foundEntry, {
- tree: foundEntry.tree.concat(tree),
+ tree: sortTree(foundEntry.tree.concat(tree)),
});
}
@@ -81,10 +87,16 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
- tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
+ tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
});
}
},
+ [types.UPDATE_TEMP_FLAG](state, { path, tempFile }) {
+ Object.assign(state.entries[path], {
+ tempFile,
+ changed: tempFile,
+ });
+ },
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
@@ -95,6 +107,47 @@ export default {
delayViewerUpdated,
});
},
+ [types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) {
+ Object.assign(state, {
+ currentActivityView,
+ });
+ },
+ [types.SET_EMPTY_STATE_SVGS](
+ state,
+ { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
+ ) {
+ Object.assign(state, {
+ emptyStateSvgPath,
+ noChangesStateSvgPath,
+ committedStateSvgPath,
+ });
+ },
+ [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
+ Object.assign(state, {
+ fileFindVisible,
+ });
+ },
+ [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
+ const changedFile = state.changedFiles.find(f => f.path === file.path);
+
+ Object.assign(state.entries[file.path], {
+ raw: file.content,
+ changed: !!changedFile,
+ staged: false,
+ lastCommit: Object.assign(state.entries[file.path].lastCommit, {
+ id: lastCommit.commit.id,
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
+ updatedAt: lastCommit.commit.authored_date,
+ }),
+ });
+ },
+ [types.BURST_UNUSED_SEAL](state) {
+ Object.assign(state, {
+ unusedSeal: false,
+ });
+ },
...projectMutations,
...mergeRequestMutation,
...fileMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
index 2972ba5e38e..f17ec4da308 100644
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -14,6 +14,10 @@ export default {
treeId: `${projectPath}/${branchName}`,
active: true,
workingReference: '',
+ commit: {
+ ...branch.commit,
+ pipeline: {},
+ },
},
},
});
@@ -23,4 +27,14 @@ export default {
workingReference: reference,
});
},
+ [types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ commit,
+ });
+ },
+ [types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) {
+ Object.assign(state.projects[projectId].branches[branchId].commit, {
+ pipeline,
+ });
+ },
};
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index eeb14b5490c..13f123b6630 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -1,9 +1,11 @@
+/* eslint-disable no-param-reassign */
import * as types from '../mutation_types';
export default {
[types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], {
active,
+ lastOpenedAt: new Date().getTime(),
});
if (active && !state.entries[path].pending) {
@@ -57,7 +59,9 @@ export default {
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
- const changed = content !== state.entries[path].raw;
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+ const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw;
+ const changed = content !== rawContent;
Object.assign(state.entries[path], {
content,
@@ -91,8 +95,10 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
Object.assign(state.entries[path], {
- content: state.entries[path].raw,
+ content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
});
},
@@ -106,38 +112,82 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
+ [types.STAGE_CHANGE](state, path) {
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: true,
+ changed: false,
+ }),
+ }),
+ });
+
+ if (stagedFile) {
+ Object.assign(stagedFile, {
+ ...state.entries[path],
+ });
+ } else {
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.concat({
+ ...state.entries[path],
+ }),
+ });
+ }
+ },
+ [types.UNSTAGE_CHANGE](state, path) {
+ const changedFile = state.changedFiles.find(f => f.path === path);
+ const stagedFile = state.stagedFiles.find(f => f.path === path);
+
+ if (!changedFile && stagedFile) {
+ Object.assign(state.entries[path], {
+ ...stagedFile,
+ key: state.entries[path].key,
+ active: state.entries[path].active,
+ opened: state.entries[path].opened,
+ changed: true,
+ });
+
+ Object.assign(state, {
+ changedFiles: state.changedFiles.concat(state.entries[path]),
+ });
+ }
+
+ Object.assign(state, {
+ stagedFiles: state.stagedFiles.filter(f => f.path !== path),
+ entries: Object.assign(state.entries, {
+ [path]: Object.assign(state.entries[path], {
+ staged: false,
+ }),
+ }),
+ });
+ },
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
- const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
- let openFiles = state.openFiles.map(f =>
- Object.assign(f, { active: f.path === file.path, opened: false }),
+ state.entries[file.path].opened = false;
+ state.entries[file.path].active = false;
+ state.entries[file.path].lastOpenedAt = new Date().getTime();
+ state.openFiles.forEach(f =>
+ Object.assign(f, {
+ opened: false,
+ active: false,
+ }),
);
-
- if (!pendingTab) {
- const openFile = openFiles.find(f => f.path === file.path);
-
- openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
- if (!f) return acc;
-
- if (f.path === file.path) {
- return acc.concat({
- ...f,
- active: true,
- pending: true,
- opened: true,
- key: `${keyPrefix}-${f.key}`,
- });
- }
-
- return acc.concat(f);
- }, []);
- }
-
- Object.assign(state, { openFiles });
+ state.openFiles = [
+ {
+ ...file,
+ key: `${keyPrefix}-${file.key}`,
+ pending: true,
+ opened: true,
+ active: true,
+ },
+ ];
},
[types.REMOVE_PENDING_TAB](state, file) {
Object.assign(state, {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index e5cc8814000..e7411f16a4f 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,8 +1,11 @@
+import { activityBarViews, viewerTypes } from '../constants';
+
export default () => ({
currentProjectId: '',
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
+ stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',
@@ -15,6 +18,9 @@ export default () => ({
rightPanelCollapsed: false,
panelResizing: false,
entries: {},
- viewer: 'editor',
+ viewer: viewerTypes.edit,
delayViewerUpdated: false,
+ currentActivityView: activityBarViews.edit,
+ unusedSeal: true,
+ fileFindVisible: false,
});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 05a019de54f..e0b9766fbee 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -15,6 +15,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
+ staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
@@ -41,6 +42,9 @@ export const dataStructure = () => ({
viewMode: 'edit',
previewMode: null,
size: 0,
+ parentPath: null,
+ lastOpenedAt: 0,
+ mrChange: null,
});
export const decorateData = entity => {
@@ -63,6 +67,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
+ parentPath = '',
} = entity;
return {
@@ -86,6 +91,7 @@ export const decorateData = entity => {
previewMode,
file_lock,
html,
+ parentPath,
};
};
@@ -101,7 +107,7 @@ export const setPageTitle = title => {
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
- actions: rootState.changedFiles.map(f => ({
+ actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
@@ -119,8 +125,8 @@ const sortTreesByTypeAndName = (a, b) => {
} else if (a.type === 'blob' && b.type === 'tree') {
return 1;
}
- if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
- if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
return 0;
};
@@ -132,3 +138,9 @@ export const sortTree = sortedTree =>
}),
)
.sort(sortTreesByTypeAndName);
+
+export const filePathMatches = (f, path) =>
+ f.path.replace(new RegExp(`${f.name}$`), '').indexOf(`${path}/`) === 0;
+
+export const getChangesCountForFiles = (files, path) =>
+ files.filter(f => filePathMatches(f, path)).length;
diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
index a1673276900..0a1c253c637 100644
--- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -6,6 +6,7 @@ self.addEventListener('message', e => {
const treeList = [];
let file;
+ let parentPath;
const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim();
@@ -17,18 +18,21 @@ self.addEventListener('message', e => {
const foundEntry = acc[folderPath];
if (!foundEntry) {
+ parentPath = parentFolder ? parentFolder.path : null;
+
const tree = decorateData({
projectId,
branchId,
id: folderPath,
name: folderName,
path: folderPath,
- url: `/${projectId}/tree/${branchId}/${folderPath}/`,
+ url: `/${projectId}/tree/${branchId}/-/${folderPath}/`,
type: 'tree',
parentTreeUrl: parentFolder ? parentFolder.url : `/${projectId}/tree/${branchId}/`,
tempFile,
changed: tempFile,
opened: tempFile,
+ parentPath,
});
Object.assign(acc, {
@@ -52,13 +56,15 @@ self.addEventListener('message', e => {
if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')];
+ parentPath = fileFolder ? fileFolder.path : null;
+
file = decorateData({
projectId,
branchId,
id: path,
name: blobName,
path,
- url: `/${projectId}/blob/${branchId}/${path}`,
+ url: `/${projectId}/blob/${branchId}/-/${path}`,
type: 'blob',
parentTreeUrl: fileFolder ? fileFolder.url : `/${projectId}/blob/${branchId}`,
tempFile,
@@ -66,6 +72,7 @@ self.addEventListener('message', e => {
content,
base64,
previewMode: viewerInformationForPath(blobName),
+ parentPath,
});
Object.assign(acc, {
@@ -86,5 +93,6 @@ self.addEventListener('message', e => {
entries,
treeList: sortTree(treeList),
file,
+ parentPath,
});
});
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 7470d634b99..f3d722409b0 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
- $block.find('.value').show();
+ $block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
- $block.find('.value').hide();
+ $block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index bb8b3d91e40..90d4e19e90b 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -30,7 +30,7 @@ export default class IssuableForm {
}
this.initAutosave();
- this.form.on('submit', this.handleSubmit);
+ this.form.on('submit:success', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index a539506bce2..7ef5e679881 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -51,7 +51,7 @@
<template>
<div class="prepend-top-default append-bottom-default clearfix">
<button
- class="btn btn-save pull-left"
+ class="btn btn-save float-left"
:class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
type="submit"
:disabled="formState.updateLoading || !isSubmitEnabled"
@@ -64,14 +64,14 @@
</i>
</button>
<button
- class="btn btn-default pull-right"
+ class="btn btn-default float-right"
type="button"
@click="closeForm">
Cancel
</button>
<button
v-if="shouldShowDeleteButton"
- class="btn btn-danger pull-right append-right-default"
+ class="btn btn-danger float-right append-right-default"
:class="{ disabled: deleteLoading }"
type="button"
:disabled="deleteLoading"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 779705e19ac..ab8bd34762f 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -84,7 +84,7 @@
<div
:class="{
'col-sm-8 col-lg-9': hasIssuableTemplates,
- 'col-xs-12': !hasIssuableTemplates,
+ 'col-12': !hasIssuableTemplates,
}"
>
<title-field
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index ace45e9dd29..c67bd7fb0c6 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import _ from 'underscore';
+import StickyFill from 'stickyfilljs';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
@@ -82,17 +83,11 @@ export default class Job {
/**
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
- then we default back to Bootstraps affix
+ then we use a polyfill
**/
if (this.$topBar.css('position') !== 'static') return;
- const offsetTop = this.$buildTrace.offset().top;
-
- this.$topBar.affix({
- offset: {
- top: offsetTop,
- },
- });
+ StickyFill.add(this.$topBar);
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 357bc9aab17..c1044f4cd42 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -1,82 +1,94 @@
<script>
- import ciHeader from '../../vue_shared/components/header_ci_component.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import callout from '../../vue_shared/components/callout.vue';
- export default {
- name: 'JobHeaderSection',
- components: {
- ciHeader,
- loadingIcon,
+export default {
+ name: 'JobHeaderSection',
+ components: {
+ ciHeader,
+ loadingIcon,
+ callout,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- data() {
- return {
- actions: this.getActions(),
- };
+ },
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+ computed: {
+ status() {
+ return this.job && this.job.status;
},
- computed: {
- status() {
- return this.job && this.job.status;
- },
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length;
- },
- /**
- * When job has not started the key will be `false`
- * When job started the key will be a string with a date.
- */
- jobStarted() {
- return !this.job.started === false;
- },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length;
},
- watch: {
- job() {
- this.actions = this.getActions();
- },
+ shouldRenderReason() {
+ return !!(this.job.status && this.job.callout_message);
},
- methods: {
- getActions() {
- const actions = [];
+ /**
+ * When job has not started the key will be `false`
+ * When job started the key will be a string with a date.
+ */
+ jobStarted() {
+ return !this.job.started === false;
+ },
+ },
+ watch: {
+ job() {
+ this.actions = this.getActions();
+ },
+ },
+ methods: {
+ getActions() {
+ const actions = [];
- if (this.job.new_issue_path) {
- actions.push({
- label: 'New issue',
- path: this.job.new_issue_path,
- cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
- type: 'link',
- });
- }
- return actions;
- },
+ if (this.job.new_issue_path) {
+ actions.push({
+ label: 'New issue',
+ path: this.job.new_issue_path,
+ cssClass: 'js-new-issue btn btn-new btn-inverted d-none d-md-block d-lg-block d-xl-block',
+ type: 'link',
+ });
+ }
+ return actions;
},
- };
+ },
+};
</script>
<template>
- <div class="js-build-header build-header top-area">
- <ci-header
- v-if="shouldRenderContent"
- :status="status"
- item-name="Job"
- :item-id="job.id"
- :time="job.created_at"
- :user="job.user"
- :actions="actions"
- :has-sidebar-button="true"
- :should-render-triggered-label="jobStarted"
- />
- <loading-icon
- v-if="isLoading"
- size="2"
- class="prepend-top-default append-bottom-default"
+ <header>
+ <div class="js-build-header build-header top-area">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Job"
+ :item-id="job.id"
+ :time="job.created_at"
+ :user="job.user"
+ :actions="actions"
+ :has-sidebar-button="true"
+ :should-render-triggered-label="jobStarted"
+ />
+ <loading-icon
+ v-if="isLoading"
+ size="2"
+ class="prepend-top-default append-bottom-default"
+ />
+ </div>
+
+ <callout
+ v-if="shouldRenderReason"
+ :message="job.callout_message"
/>
- </div>
+ </header>
</template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index dfe87d89a39..83560a8ff0e 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -39,7 +39,7 @@
<span
v-if="hasHelpURL"
- class="help-button pull-right"
+ class="help-button float-right"
>
<a
:href="helpUrl"
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index af47056d98f..82fec7e936b 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -1,80 +1,119 @@
<script>
- import detailRow from './sidebar_detail_row.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import timeagoMixin from '../../vue_shared/mixins/timeago';
- import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
+import detailRow from './sidebar_detail_row.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
- export default {
- name: 'SidebarDetailsBlock',
- components: {
- detailRow,
- loadingIcon,
+export default {
+ name: 'SidebarDetailsBlock',
+ components: {
+ detailRow,
+ loadingIcon,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
- mixins: [
- timeagoMixin,
- ],
- props: {
- job: {
- type: Object,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- runnerHelpUrl: {
- type: String,
- required: false,
- default: '',
- },
+ isLoading: {
+ type: Boolean,
+ required: true,
},
- computed: {
- shouldRenderContent() {
- return !this.isLoading && Object.keys(this.job).length > 0;
- },
- coverage() {
- return `${this.job.coverage}%`;
- },
- duration() {
- return timeIntervalInWords(this.job.duration);
- },
- queued() {
- return timeIntervalInWords(this.job.queued);
- },
- runnerId() {
- return `#${this.job.runner.id}`;
- },
- hasTimeout() {
- return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
- },
- timeout() {
- if (this.job.metadata == null) {
- return '';
- }
+ canUserRetry: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ runnerHelpUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.job).length > 0;
+ },
+ coverage() {
+ return `${this.job.coverage}%`;
+ },
+ duration() {
+ return timeIntervalInWords(this.job.duration);
+ },
+ queued() {
+ return timeIntervalInWords(this.job.queued);
+ },
+ runnerId() {
+ return `${this.job.runner.description} (#${this.job.runner.id})`;
+ },
+ retryButtonClass() {
+ let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
+ className +=
+ this.job.status && this.job.recoverable
+ ? ' btn-primary'
+ : ' btn-inverted-secondary';
+ return className;
+ },
+ hasTimeout() {
+ return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
+ },
+ timeout() {
+ if (this.job.metadata == null) {
+ 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;
- },
- renderBlock() {
- return this.job.merge_request ||
- this.job.duration ||
- this.job.finished_data ||
- this.job.erased_at ||
- this.job.queued ||
- this.job.runner ||
- this.job.coverage ||
- this.job.tags.length ||
- this.job.cancel_path;
- },
+ return t;
},
- };
+ renderBlock() {
+ return (
+ this.job.merge_request ||
+ this.job.duration ||
+ this.job.finished_data ||
+ this.job.erased_at ||
+ this.job.queued ||
+ this.job.runner ||
+ this.job.coverage ||
+ this.job.tags.length ||
+ this.job.cancel_path
+ );
+ },
+ },
+};
</script>
<template>
<div>
+ <div class="block">
+ <strong class="inline prepend-top-8">
+ {{ job.name }}
+ </strong>
+ <a
+ v-if="canUserRetry"
+ :class="retryButtonClass"
+ :href="job.retry_path"
+ data-method="post"
+ rel="nofollow"
+ >
+ {{ __('Retry') }}
+ </a>
+ <button
+ type="button"
+ :aria-label="__('Toggle Sidebar')"
+ class="btn btn-blank gutter-toggle pull-right
+ d-block d-sm-block d-md-none js-sidebar-build-toggle"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-angle-double-right"
+ ></i>
+ </button>
+ </div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
@@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
- New issue
+ {{ __('New issue') }}
</a>
<a
- v-if="job.retry_path"
+ v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
- Retry
+ {{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
@@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
- Merge Request:
+ {{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
@@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
- Tags:
+ {{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
@@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
- Cancel
+ {{ __('Cancel') }}
</a>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 656676ead91..f2939ad4dbe 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
+ const detailsBlockElement = document.getElementById('js-details-block-vue');
+ const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
- el: '#js-details-block-vue',
+ el: detailsBlockElement,
components: {
detailsBlock,
},
@@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
+ canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 8013df53f25..a8a314abd6b 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -36,7 +36,7 @@ export default class LabelManager {
const $label = $(`#${$btn.data('domId')}`);
const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
- $tooltip.tooltip('destroy');
+ $tooltip.tooltip('dispose');
_this.toggleLabelPriority($label, action);
_this.toggleEmptyState($label, $btn, action);
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index d0050abb8e9..eafdaf4a672 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data)
.then(({ data }) => {
- var labelCount, template, labelTooltipTitle, labelTitles;
+ var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@@ -115,13 +115,12 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', ');
}
else {
- labelTooltipTitle = '';
- $sidebarLabelTooltip.tooltip('destroy');
+ labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
- .tooltip('fixTitle');
+ .tooltip('_fixTitle');
$('.has-tooltip', $value).tooltip({
container: 'body'
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9ff2042475b..8b5445d012b 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -51,7 +51,7 @@ export const rstrip = (val) => {
return val;
};
-export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('fixTitle');
+export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle');
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index c3d94d63c13..0ff23bbb061 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,10 +2,7 @@ import $ from 'jquery';
import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
-import {
- languageCode,
- s__,
-} from '../../locale';
+import { languageCode, s__ } from '../../locale';
window.timeago = timeago;
window.dateFormat = dateFormat;
@@ -17,11 +14,37 @@ window.dateFormat = dateFormat;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = (abbreviated) => {
+const getMonthNames = abbreviated => {
if (abbreviated) {
- return [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+ return [
+ s__('Jan'),
+ s__('Feb'),
+ s__('Mar'),
+ s__('Apr'),
+ s__('May'),
+ s__('Jun'),
+ s__('Jul'),
+ s__('Aug'),
+ s__('Sep'),
+ s__('Oct'),
+ s__('Nov'),
+ s__('Dec'),
+ ];
}
- return [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
+ return [
+ s__('January'),
+ s__('February'),
+ s__('March'),
+ s__('April'),
+ s__('May'),
+ s__('June'),
+ s__('July'),
+ s__('August'),
+ s__('September'),
+ s__('October'),
+ s__('November'),
+ s__('December'),
+ ];
};
/**
@@ -29,7 +52,8 @@ const getMonthNames = (abbreviated) => {
* @param {date} date
* @returns {String}
*/
-export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
+export const getDayName = date =>
+ ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()];
/**
* @example
@@ -55,7 +79,7 @@ export function getTimeago() {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
@@ -73,7 +97,7 @@ export function getTimeago() {
};
const locale = function getLocale(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')],
+ [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
[s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
@@ -102,7 +126,7 @@ export function getTimeago() {
* For the given element, renders a timeago instance.
* @param {jQuery} $els
*/
-export const renderTimeago = ($els) => {
+export const renderTimeago = $els => {
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
// timeago.js sets timeouts internally for each timeago value to be updated in real time
@@ -119,7 +143,7 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
if (setTimeago) {
// Recreate with custom template
$(el).tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
});
}
@@ -141,7 +165,9 @@ export const timeFor = (time, expiredLabel) => {
if (new Date(time) < new Date()) {
return expiredLabel || s__('Timeago|Past due');
}
- return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim();
+ return getTimeago()
+ .format(time, `${timeagoLanguageCode}-remaining`)
+ .trim();
};
export const getDayDifference = (a, b) => {
@@ -161,7 +187,7 @@ export const getDayDifference = (a, b) => {
export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
- const seconds = secondsInteger - (minutes * 60);
+ const seconds = secondsInteger - minutes * 60;
let text = '';
if (minutes >= 1) {
@@ -178,8 +204,34 @@ export function dateInWords(date, abbreviated = false, hideYear = false) {
const month = date.getMonth();
const year = date.getFullYear();
- const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
- const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
+ const monthNames = [
+ s__('January'),
+ s__('February'),
+ s__('March'),
+ s__('April'),
+ s__('May'),
+ s__('June'),
+ s__('July'),
+ s__('August'),
+ s__('September'),
+ s__('October'),
+ s__('November'),
+ s__('December'),
+ ];
+ const monthNamesAbbr = [
+ s__('Jan'),
+ s__('Feb'),
+ s__('Mar'),
+ s__('Apr'),
+ s__('May'),
+ s__('Jun'),
+ s__('Jul'),
+ s__('Aug'),
+ s__('Sep'),
+ s__('Oct'),
+ s__('Nov'),
+ s__('Dec'),
+ ];
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
@@ -210,7 +262,7 @@ export const monthInWords = (date, abbreviated = false) => {
*
* @param {Date} date
*/
-export const totalDaysInMonth = (date) => {
+export const totalDaysInMonth = date => {
if (!date) {
return 0;
}
@@ -223,12 +275,20 @@ export const totalDaysInMonth = (date) => {
*
* @param {Date} date
*/
-export const getSundays = (date) => {
+export const getSundays = date => {
if (!date) {
return [];
}
- const daysToSunday = ['Saturday', 'Friday', 'Thursday', 'Wednesday', 'Tuesday', 'Monday', 'Sunday'];
+ const daysToSunday = [
+ 'Saturday',
+ 'Friday',
+ 'Thursday',
+ 'Wednesday',
+ 'Tuesday',
+ 'Monday',
+ 'Sunday',
+ ];
const month = date.getMonth();
const year = date.getFullYear();
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
new file mode 100644
index 00000000000..5e0f9b612a2
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -0,0 +1,4 @@
+export const UP_KEY_CODE = 38;
+export const DOWN_KEY_CODE = 40;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index b54ecd2d543..5e786ee6935 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @returns {String}
*/
-export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
+export const stripHtml = (string, replace = '') => {
+ if (!string) return string;
+
+ return string.replace(/<[^>]*>/g, replace);
+};
/**
* Converts snake_case string to camelCase
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2c80baba10b..9803bebfd10 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,22 +1,19 @@
-/* eslint-disable import/first */
/* global $ */
import jQuery from 'jquery';
import Cookies from 'js-cookie';
import svg4everybody from 'svg4everybody';
-// expose common libraries as globals (TODO: remove these)
-window.jQuery = jQuery;
-window.$ = jQuery;
+// bootstrap webpack, common libs, polyfills, and behaviors
+import './webpack';
+import './commons';
+import './behaviors';
// lib/utils
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
-// behaviors
-import './behaviors/';
-
// everything else
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
@@ -31,9 +28,12 @@ import initLogoAnimation from './logo';
import './milestone_select';
import './projects_dropdown';
import initBreadcrumbs from './breadcrumb';
-
import initDispatcher from './dispatcher';
+// expose jQuery as global (TODO: remove these)
+window.jQuery = jQuery;
+window.$ = jQuery;
+
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
@@ -46,16 +46,20 @@ document.addEventListener('beforeunload', () => {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
- $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('dispose');
// Close any open popover
- $('[data-toggle="popover"]').popover('destroy');
+ $('[data-toggle="popover"]').popover('dispose');
});
window.addEventListener('hashchange', handleLocationHash);
-window.addEventListener('load', function onLoad() {
- window.removeEventListener('load', onLoad, false);
- handleLocationHash();
-}, false);
+window.addEventListener(
+ 'load',
+ function onLoad() {
+ window.removeEventListener('load', onLoad, false);
+ handleLocationHash();
+ },
+ false,
+);
gl.lazyLoader = new LazyLoader({
scrollContainer: window,
@@ -89,9 +93,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .layout-page');
- $rightSidebar
- .removeClass('right-sidebar-expanded')
- .addClass('right-sidebar-collapsed');
+ $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
}
// prevent default action for disabled buttons
@@ -108,7 +110,8 @@ document.addEventListener('DOMContentLoaded', () => {
addSelectOnFocusBehaviour('.js-select-on-focus');
$('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() {
- $(this).tooltip('destroy')
+ $(this)
+ .tooltip('dispose')
.closest('li')
.fadeOut();
});
@@ -118,7 +121,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
$('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() {
- $(this).closest('tr').fadeOut();
+ $(this)
+ .closest('tr')
+ .fadeOut();
});
// Initialize select2 selects
@@ -136,9 +141,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
// Initialize tooltips
- $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
+ trigger: 'hover',
placement(tip, el) {
return $(el).data('placement') || 'bottom';
},
@@ -155,7 +160,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Form submitter
$('.trigger-submit').on('change', function triggerSubmitCallback() {
- $(this).parents('form').submit();
+ $(this)
+ .parents('form')
+ .submit();
});
localTimeAgo($('abbr.timeago, .js-timeago'), true);
@@ -189,7 +196,7 @@ document.addEventListener('DOMContentLoaded', () => {
$container.remove();
});
- $('.navbar-toggle').on('click', () => {
+ $('.navbar-toggler').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
gl.lazyLoader.loadCheck();
});
@@ -204,9 +211,15 @@ document.addEventListener('DOMContentLoaded', () => {
$this.toggleClass('active');
if ($this.hasClass('active')) {
- notesHolders.show().find('.hide, .content').show();
+ notesHolders
+ .show()
+ .find('.hide, .content')
+ .show();
} else {
- notesHolders.hide().find('.content').hide();
+ notesHolders
+ .hide()
+ .find('.content')
+ .hide();
}
$(document).trigger('toggle.comments');
@@ -247,9 +260,11 @@ document.addEventListener('DOMContentLoaded', () => {
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
- flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
- removeFlashClickListener(flashEl);
- });
+ flashContainer
+ .querySelectorAll('.flash-alert, .flash-notice, .flash-success')
+ .forEach(flashEl => {
+ removeFlashClickListener(flashEl);
+ });
}
initDispatcher();
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index db1d09eb2f2..70f185e3656 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -351,7 +351,7 @@ import Cookies from 'js-cookie';
},
getCommitButtonText() {
- const initial = 'Commit conflict resolution';
+ const initial = 'Commit to source branch';
const inProgress = 'Committing...';
return this.state ? this.state.isSubmitting ? inProgress : initial : initial;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3f84f4b9499..3548c07aea8 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
- $('.mr-loading-status .loading').toggle(status);
+ $('.mr-loading-status .loading').toggleClass('hidden', status);
}
diffViewType() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d0a2b27b0e6..f8b3d3061f0 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -4,6 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@@ -11,7 +12,8 @@ import ModalStore from './boards/stores/modal_store';
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
if (currentProject !== null) {
- this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
+ this.currentProject =
+ typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
this.init(els, options);
@@ -25,7 +27,10 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
- let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
+ let milestoneLinkNoneTemplate,
+ milestoneLinkTemplate,
+ selectedMilestone,
+ selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
@@ -45,46 +50,47 @@ export default class MilestoneSelect {
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
- selectedMilestoneDefault = (showAny ? '' : null);
- selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
+ selectedMilestoneDefault = showAny ? '' : null;
+ selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
- milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkTemplate = _.template(
+ '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
+ );
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
- collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
- data: (term, callback) => axios.get(milestonesUrl)
- .then(({ data }) => {
+ data: (term, callback) =>
+ axios.get(milestonesUrl).then(({ data }) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
- id: 0,
- name: '',
- title: 'Any Milestone'
+ id: null,
+ name: null,
+ title: 'Any Milestone',
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
- title: 'No Milestone'
+ title: 'No Milestone',
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
- title: 'Upcoming'
+ title: 'Upcoming',
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
- title: 'Started'
+ title: 'Started',
});
}
if (extraOptions.length) {
@@ -106,7 +112,7 @@ export default class MilestoneSelect {
`,
filterable: true,
search: {
- fields: ['title']
+ fields: ['title'],
},
selectable: true,
toggleLabel: (selected, el, e) => {
@@ -119,7 +125,7 @@ export default class MilestoneSelect {
defaultLabel: defaultLabel,
fieldName: $dropdown.data('fieldName'),
text: milestone => _.escape(milestone.title),
- id: (milestone) => {
+ id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
@@ -131,7 +137,7 @@ export default class MilestoneSelect {
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
- opened: (e) => {
+ opened: e => {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
@@ -140,7 +146,7 @@ export default class MilestoneSelect {
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
- clicked: (clickEvent) => {
+ clicked: clickEvent => {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
@@ -155,11 +161,14 @@ export default class MilestoneSelect {
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = (page === page && page === 'projects:merge_requests:index');
- const isSelecting = (selected.name !== selectedMilestone);
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
+ const isSelecting = selected.name !== selectedMilestone;
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
- if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ if (
+ $dropdown.hasClass('js-filter-bulk-update') ||
+ $dropdown.hasClass('js-issuable-form-dropdown')
+ ) {
e.preventDefault();
return;
}
@@ -177,10 +186,13 @@ export default class MilestoneSelect {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
- gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
- id: selected.id,
- title: selected.name
- }));
+ gl.issueBoards.boardStoreIssueSet(
+ 'milestone',
+ new ListMilestone({
+ id: selected.id,
+ title: selected.name,
+ }),
+ );
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
@@ -188,7 +200,8 @@ export default class MilestoneSelect {
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
- gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ gl.issueBoards.BoardsStore.detail.issue
+ .update($dropdown.attr('data-issue-update'))
.then(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -203,7 +216,8 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
- return axios.put(issueUpdateURL, data)
+ return axios
+ .put(issueUpdateURL, data)
.then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
@@ -214,17 +228,26 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
- return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ return $sidebarCollapsedValue
+ .attr(
+ 'data-original-title',
+ `${data.milestone.name}<br />${data.milestone.remaining}`,
+ )
+ .find('span')
+ .text(data.milestone.title);
} else {
$value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue.find('span').text('No');
+ return $sidebarCollapsedValue
+ .attr('data-original-title', __('Milestone'))
+ .find('span')
+ .text(__('None'));
}
})
.catch(() => {
$loading.fadeOut();
});
}
- }
+ },
});
});
}
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 01399de4c62..f8257b6abab 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-new */
-
import $ from 'jquery';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -62,7 +60,7 @@ export default class MiniPipelineGraph {
*/
renderBuildsList(stageContainer, data) {
const dropdownContainer = stageContainer.parentElement.querySelector(
- `${this.dropdownListSelector} .js-builds-dropdown-list`,
+ `${this.dropdownListSelector} .js-builds-dropdown-list ul`,
);
dropdownContainer.innerHTML = data;
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index f93b1da4f58..de6755e0414 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -81,9 +81,8 @@ export default {
time: new Date(),
value: 0,
},
- currentDataIndex: 0,
currentXCoordinate: 0,
- currentFlagPosition: 0,
+ currentCoordinates: [],
showFlag: false,
showFlagContent: false,
timeSeries: [],
@@ -273,6 +272,9 @@ export default {
:line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
+ :current-coordinates="currentCoordinates[index]"
+ :current-time-series-index="index"
+ :show-dot="showFlagContent"
/>
<graph-deployment
:deployment-data="reducedDeploymentData"
@@ -298,9 +300,9 @@ export default {
:show-flag-content="showFlagContent"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
- :current-data-index="currentDataIndex"
:legend-title="legendTitle"
:deployment-flag-data="deploymentFlagData"
+ :current-coordinates="currentCoordinates"
/>
</div>
<graph-legend
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index b8202e25685..8a771107de8 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -47,14 +47,14 @@ export default {
type: String,
required: true,
},
- currentDataIndex: {
- type: Number,
- required: true,
- },
legendTitle: {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Array,
+ required: true,
+ },
},
computed: {
formatTime() {
@@ -90,10 +90,12 @@ export default {
},
},
methods: {
- seriesMetricValue(series) {
+ seriesMetricValue(seriesIndex, series) {
+ const indexFromCoordinates = this.currentCoordinates[seriesIndex]
+ ? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
const index = this.deploymentFlagData
? this.deploymentFlagData.seriesIndex
- : this.currentDataIndex;
+ : indexFromCoordinates;
const value = series.values[index] && series.values[index].value;
if (isNaN(value)) {
return '-';
@@ -128,7 +130,7 @@ export default {
<h5 v-if="deploymentFlagData">
Deployed
</h5>
- {{ formatDate }} at
+ {{ formatDate }}
<strong>{{ formatTime }}</strong>
</div>
<div
@@ -163,9 +165,11 @@ export default {
:key="index"
>
<track-line :track="series"/>
- <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td>
- <strong>{{ seriesMetricValue(series) }}</strong>
+ {{ series.track }} {{ seriesMetricLabel(index, series) }}
+ </td>
+ <td>
+ <strong>{{ seriesMetricValue(index, series) }}</strong>
</td>
</tr>
</table>
diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue
index 881560124a5..52f8aa2ee3f 100644
--- a/app/assets/javascripts/monitoring/components/graph/path.vue
+++ b/app/assets/javascripts/monitoring/components/graph/path.vue
@@ -22,6 +22,15 @@ export default {
type: String,
required: true,
},
+ currentCoordinates: {
+ type: Object,
+ required: false,
+ default: () => ({ currentX: 0, currentY: 0 }),
+ },
+ showDot: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
strokeDashArray() {
@@ -33,12 +42,20 @@ export default {
};
</script>
<template>
- <g>
+ <g transform="translate(-5, 20)">
+ <circle
+ class="circle-path"
+ :cx="currentCoordinates.currentX"
+ :cy="currentCoordinates.currentY"
+ :fill="lineColor"
+ :stroke="lineColor"
+ r="3"
+ v-if="showDot"
+ />
<path
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
- transform="translate(-5, 20)"
/>
<path
class="metric-line"
@@ -47,7 +64,6 @@ export default {
fill="none"
stroke-width="1"
:stroke-dasharray="strokeDashArray"
- transform="translate(-5, 20)"
/>
</g>
</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue
index 79b322e2e42..18be65fd1ef 100644
--- a/app/assets/javascripts/monitoring/components/graph/track_line.vue
+++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue
@@ -19,16 +19,16 @@ export default {
<template>
<td>
<svg
- width="15"
- height="6">
+ width="16"
+ height="8">
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
stroke-width="4"
:x1="0"
- :x2="15"
- :y1="2"
- :y2="2"
+ :x2="16"
+ :y1="4"
+ :y2="4"
/>
</svg>
</td>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index a6dbe42a8f0..241627f9790 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -17,12 +17,12 @@ export default {
<template>
<div
v-if="showPanels"
- class="panel panel-default prometheus-panel"
+ class="card prometheus-panel"
>
- <div class="panel-heading">
+ <div class="card-header">
<h4>{{ name }}</h4>
</div>
- <div class="panel-body prometheus-graph-group">
+ <div class="card-body prometheus-graph-group">
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 6cc67ba57ee..4f23814ff3e 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -52,14 +52,22 @@ const mixins = {
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+
this.currentData = timeSeries.values[hoveredDataIndex];
- this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
- if (this.currentXCoordinate > (this.graphWidth - 200)) {
- this.currentFlagPosition = this.currentXCoordinate - 103;
- } else {
- this.currentFlagPosition = this.currentXCoordinate;
- }
+
+ this.currentCoordinates = this.timeSeries.map((series) => {
+ const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
+ const currentData = series.values[currentDataIndex];
+ const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
+ const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
+
+ return {
+ currentX,
+ currentY,
+ currentDataIndex,
+ };
+ });
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index f3c9acdd93e..d88c13609dc 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -14,7 +14,7 @@ const d3 = {
timeYear,
};
-export const dateFormat = d3.time('%a, %b %-d');
+export const dateFormat = d3.time('%d %b %Y, ');
export const timeFormat = d3.time('%-I:%M%p');
export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 8a93c7e6bae..4d3f1f1a7cc 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -123,6 +123,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
+ timeSeriesScaleY,
values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 2121907dff0..b2c1a26bbae 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1231,8 +1231,8 @@ export default class Notes {
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
- targetRow.toggle(showNow);
- notesContent.toggle(showNow);
+ targetRow.toggleClass('hide', !showNow);
+ notesContent.toggleClass('hide', !showNow);
}
if (addForm) {
@@ -1427,7 +1427,7 @@ export default class Notes {
const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in');
- $container.find('tbody').prepend(lines);
+ $container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder);
@@ -1675,7 +1675,7 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="hidden-xs">${_.escape(
+ <span class="d-none d-sm-block">${_.escape(
currentUsername,
)}</span>
<span class="note-headline-light">${_.escape(
@@ -1694,7 +1694,7 @@ export default class Notes {
</li>`,
);
- $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
+ $tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname));
$tempNote
.find('.note-headline-light')
.text(`@${_.escape(currentUsername)}`);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 396a675b4ac..72d7e22fba0 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -99,10 +99,6 @@ export default {
'js-note-target-reopen': !this.isOpen,
};
},
- supportQuickActions() {
- // Disable quick actions support for Epics
- return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
- },
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
@@ -325,7 +321,7 @@ Please check your network connection and try again.`;
<li class="timeline-entry">
<div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon hidden-xs hidden-sm">
+ <div class="timeline-icon d-none d-sm-none d-md-block">
<user-avatar-link
v-if="author"
:link-href="author.path"
@@ -359,7 +355,7 @@ Please check your network connection and try again.`;
name="note[note]"
class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
- :data-supports-quick-actions="supportQuickActions"
+ data-supports-quick-actions="true"
aria-label="Description"
v-model="note"
ref="textarea"
@@ -373,7 +369,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea"
</markdown-field>
<div class="note-form-actions">
<div
- class="pull-left btn-group
+ class="float-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button
@click.prevent="handleSave()"
@@ -387,6 +383,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
name="button"
type="button"
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
+ data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown">
<i
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index d492d1cd001..cbe4774a360 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -86,7 +86,7 @@ export default {
v-html="resolveSvg"
></span>
</span>
- <span class=".line-resolve-text">
+ <span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
</span>
</div>
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index 4ddca918495..2dc39d1a186 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -32,17 +32,17 @@ export default {
<template>
<div :class="className">
{{ actionText }}
- <time-ago-tooltip
- :time="editedAt"
- tooltip-placement="bottom"
- />
<template v-if="editedBy">
- by
+ {{ s__('ByAuthor|by') }}
<a
:href="editedBy.path"
class="js-vue-author author_link">
{{ editedBy.name }}
</a>
</template>
+ <time-ago-tooltip
+ :time="editedAt"
+ tooltip-placement="bottom"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index c3d1ef1fcc6..a4081957207 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -62,6 +62,21 @@ export default {
<template>
<div class="note-header-info">
+ <div
+ v-if="includeToggle"
+ class="discussion-actions">
+ <button
+ @click="handleToggle"
+ class="note-action-button discussion-toggle-button js-vue-toggle-button"
+ type="button">
+ <i
+ :class="toggleChevronClass"
+ class="fa"
+ aria-hidden="true">
+ </i>
+ {{ __('Toggle discussion') }}
+ </button>
+ </div>
<a :href="author.path">
<span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light">
@@ -78,10 +93,13 @@ export default {
v-html="actionTextHtml"
class="system-note-message">
</span>
+ <span class="system-note-separator">
+ &middot;
+ </span>
<a
:href="noteTimestampLink"
@click="updateTargetNoteHash"
- class="note-timestamp">
+ class="note-timestamp system-note-separator">
<time-ago-tooltip
:time="createdAt"
tooltip-placement="bottom"
@@ -95,20 +113,5 @@ export default {
</i>
</span>
</span>
- <div
- v-if="includeToggle"
- class="discussion-actions">
- <button
- @click="handleToggle"
- class="note-action-button discussion-toggle-button js-vue-toggle-button"
- type="button">
- <i
- :class="toggleChevronClass"
- class="fa"
- aria-hidden="true">
- </i>
- Toggle discussion
- </button>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index e0f883a8e08..7f5aacaa3a2 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -100,7 +100,7 @@ export default {
: 'div';
},
wrapperClass() {
- return this.isDiffDiscussion ? '' : 'panel panel-default';
+ return this.isDiffDiscussion ? '' : 'card';
},
},
mounted() {
@@ -263,7 +263,7 @@ Please check your network connection and try again.`;
class="discussion-reply-holder">
<template v-if="!isReplying && canReply">
<div
- class="btn-group-justified discussion-with-resolve-btn"
+ class="btn-group d-flex discussion-with-resolve-btn"
role="group">
<div
class="btn-group"
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 244a6980b5a..98ce070288e 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
scrollToElement(el);
}
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index f89591a54d6..787be6f4c99 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length;
};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 9e6cf67dff0..00e27ca0e70 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -15,7 +15,7 @@ export default class NotificationsForm {
toggleCheckbox(e) {
const $checkbox = $(e.currentTarget);
- const $parent = $checkbox.closest('.checkbox');
+ const $parent = $checkbox.closest('.form-check');
this.saveEvent($checkbox, $parent);
}
diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js
index 91f154b7ecd..ff4d6ab15f9 100644
--- a/app/assets/javascripts/pages/admin/admin.js
+++ b/app/assets/javascripts/pages/admin/admin.js
@@ -25,7 +25,7 @@ export default function adminInit() {
$('body').on('click', '.js-toggle-colors-link', (e) => {
e.preventDefault();
- $('.js-toggle-colors-container').toggle();
+ $('.js-toggle-colors-container').toggleClass('hide');
});
$('.log-tabs a').on('click', function logTabsClick(e) {
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 0e3ac636661..9ce176744ba 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
@@ -52,16 +52,15 @@
text() {
const keepContributionsText = 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.
+ 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|
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".
+ 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>`,
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 04a0d8117cc..d3b2656743d 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
@@ -1,6 +1,10 @@
+import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+
const variableListEl = document.querySelector('.js-ci-variable-list-section');
// eslint-disable-next-line no-new
new AjaxVariableList({
diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index.js
new file mode 100644
index 00000000000..efadf6967aa
--- /dev/null
+++ b/app/assets/javascripts/pages/ide/index.js
@@ -0,0 +1,9 @@
+import { initIde, resetServiceWorkersPublicPath } from '~/ide/index';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const ideElement = document.getElementById('ide');
+ if (ideElement) {
+ resetServiceWorkersPublicPath();
+ initIde(ideElement);
+ }
+});
diff --git a/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
new file mode 100644
index 00000000000..edd7e38471b
--- /dev/null
+++ b/app/assets/javascripts/pages/ldap/omniauth_callbacks/index.js
@@ -0,0 +1,3 @@
+import initU2F from '../../../shared/sessions/u2f';
+
+document.addEventListener('DOMContentLoaded', initU2F);
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 fbdef329ab2..8e8f47c21d8 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,7 @@ 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-xs btn-warning pull-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/clusters/gcp/login/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
new file mode 100644
index 00000000000..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/gcp/login/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
new file mode 100644
index 00000000000..0c2d7d7c96a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/new/index.js
@@ -0,0 +1,3 @@
+import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
+
+gcpSignupOffer();
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
index d1c78bd61db..768da8fb236 100644
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ b/app/assets/javascripts/pages/projects/compare/index.js
@@ -1,3 +1,3 @@
import initCompareAutocomplete from '~/compare_autocomplete';
-document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
+document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index 2b4fd3c47c0..a626ed2d30b 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -1,8 +1,10 @@
import Diff from '~/diff';
import initChangesDropdown from '~/init_changes_dropdown';
+import GpgBadges from '~/gpg_badges';
document.addEventListener('DOMContentLoaded', () => {
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
+ GpgBadges.fetch();
});
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 a99ce0f1c36..5316d3e9f3c 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
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
+/* eslint-disable func-names, space-before-function-paren, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import $ from 'jquery';
import _ from 'underscore';
@@ -13,17 +13,17 @@ import { dateTickFormat } from '~/lib/utils/tick_formats';
const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
-const extend = function(child, parent) { for (var 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 hasProp = {}.hasOwnProperty;
+const extend = function(child, parent) { for (const key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
export const ContributorsGraph = (function() {
function ContributorsGraph() {}
ContributorsGraph.prototype.MARGIN = {
top: 20,
- right: 20,
+ right: 10,
bottom: 30,
- left: 50
+ left: 40
};
ContributorsGraph.prototype.x_domain = null;
@@ -32,6 +32,12 @@ 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 marginWidth = this.MARGIN.left + this.MARGIN.right;
+ return baseWidth - parentPaddingWidth - marginWidth;
+ };
+
ContributorsGraph.set_x_domain = function(data) {
return ContributorsGraph.prototype.x_domain = data;
};
@@ -105,11 +111,10 @@ export const ContributorsMasterGraph = (function(superClass) {
function ContributorsMasterGraph(data1) {
const $parentElement = $('#contributors-master');
- const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right'));
this.data = data1;
this.update_content = this.update_content.bind(this);
- this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right);
+ this.width = this.determine_width($('.js-graphs-show').width(), $parentElement);
this.height = 200;
this.x = null;
this.y = null;
@@ -122,8 +127,7 @@ export const ContributorsMasterGraph = (function(superClass) {
}
ContributorsMasterGraph.prototype.process_dates = function(data) {
- var dates;
- dates = this.get_dates(data);
+ const dates = this.get_dates(data);
this.parse_dates(data);
return ContributorsGraph.set_dates(dates);
};
@@ -133,8 +137,7 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.parse_dates = function(data) {
- var parseDate;
- parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
@@ -152,7 +155,14 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_svg = function() {
- return 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) {
@@ -218,12 +228,14 @@ export const ContributorsAuthorGraph = (function(superClass) {
extend(ContributorsAuthorGraph, superClass);
function ContributorsAuthorGraph(data1) {
+ const $parentElements = $('.person');
+
this.data = data1;
// Don't split graph size in half for mobile devices.
- if ($(window).width() < 768) {
- this.width = $('.content').width() - 80;
+ if ($(window).width() < 790) {
+ this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
} else {
- this.width = ($('.content').width() / 2) - 100;
+ this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
}
this.height = 200;
this.x = null;
@@ -249,8 +261,7 @@ export const ContributorsAuthorGraph = (function(superClass) {
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
return this.area = d3.area().x(function(d) {
- var parseDate;
- parseDate = d3.timeParse("%Y-%m-%d");
+ const parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
@@ -264,9 +275,16 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_svg = function() {
- var persons = document.querySelectorAll('.person');
+ const persons = document.querySelectorAll('.person');
this.list_item = persons[persons.length - 1];
- return 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) {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
new file mode 100644
index 00000000000..46f3f55a400
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -0,0 +1,60 @@
+import $ from 'jquery';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+import initCompareAutocomplete from '~/compare_autocomplete';
+import initTargetProjectDropdown from './target_project_dropdown';
+
+const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
+ $loadingIndicator.show();
+ $commitList.empty();
+
+ return axios
+ .get(url, {
+ params,
+ })
+ .then(({ data }) => {
+ $loadingIndicator.hide();
+ $commitList.html(data);
+ localTimeAgo($('.js-timeago', $commitList));
+ });
+};
+
+export default mrNewCompareNode => {
+ const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
+ initTargetProjectDropdown();
+
+ const updateSourceBranchCommitList = () =>
+ updateCommitList(
+ sourceBranchUrl,
+ $(mrNewCompareNode).find('.js-source-loading'),
+ $(mrNewCompareNode).find('.mr_source_commit'),
+ {
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[source_branch]']")
+ .val(),
+ },
+ );
+ const updateTargetBranchCommitList = () =>
+ updateCommitList(
+ targetBranchUrl,
+ $(mrNewCompareNode).find('.js-target-loading'),
+ $(mrNewCompareNode).find('.mr_target_commit'),
+ {
+ target_project_id: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_project_id]']")
+ .val(),
+ ref: $(mrNewCompareNode)
+ .find("input[name='merge_request[target_branch]']")
+ .val(),
+ },
+ );
+ initCompareAutocomplete('branches', $dropdown => {
+ if ($dropdown.is('.js-target-branch')) {
+ updateTargetBranchCommitList();
+ } else if ($dropdown.is('.js-source-branch')) {
+ updateSourceBranchCommitList();
+ }
+ });
+ updateSourceBranchCommitList();
+ updateTargetBranchCommitList();
+};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 6c9afddefac..01a0b4870c1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,18 +1,15 @@
-import Compare from '~/compare';
import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initCompare from './compare';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
- new Compare({ // eslint-disable-line no-new
- targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
- sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
- targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
- });
+ initCompare(mrNewCompareNode);
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
- new MergeRequest({ // eslint-disable-line no-new
+ // eslint-disable-next-line no-new
+ new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
initPipelines();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
new file mode 100644
index 00000000000..b72fe6681df
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js
@@ -0,0 +1,22 @@
+import $ from 'jquery';
+
+export default () => {
+ const $targetProjectDropdown = $('.js-target-project');
+ $targetProjectDropdown.glDropdown({
+ selectable: true,
+ fieldName: $targetProjectDropdown.data('fieldName'),
+ filterable: true,
+ id(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked({ $el }) {
+ $('.mr_target_commit').empty();
+ const $targetBranchDropdown = $('.js-target-branch');
+ $targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
+ $targetBranchDropdown.data('glDropdown').clearMenu();
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index 9aa8945e268..b0b077a5e4c 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,6 +1,12 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
+import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
document.addEventListener('DOMContentLoaded', () => {
new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+
+ setupNativeFormVariableList({
+ container: $('.js-ci-variable-list-section'),
+ formField: 'variables_attributes',
+ });
});
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 25a88f846eb..17b91479ea5 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
@@ -42,7 +42,7 @@
</label>
<span
v-if="helpText"
- class="help-block"
+ class="form-text text-muted"
>
{{ helpText }}
</span>
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
new file mode 100644
index 00000000000..df21e2f8771
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -0,0 +1,77 @@
+<script>
+import _ from 'underscore';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ deleteWikiUrl: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ pageTitle: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ csrfToken: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ },
+ computed: {
+ message() {
+ return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?');
+ },
+ title() {
+ return sprintf(
+ s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'),
+ {
+ pageTitle: _.escape(this.pageTitle),
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ id="delete-wiki-modal"
+ :header-title-text="title"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')"
+ @submit="onSubmit"
+ >
+ {{ message }}
+ <form
+ ref="form"
+ :action="deleteWikiUrl"
+ method="post"
+ class="form-horizontal js-requires-input"
+ >
+ <input
+ ref="method"
+ type="hidden"
+ name="_method"
+ value="delete"
+ />
+ <input
+ type="hidden"
+ name="authenticity_token"
+ :value="csrfToken"
+ />
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index ec01c66ffda..0295653cb29 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,12 +1,40 @@
import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import csrf from '~/lib/utils/csrf';
import Wikis from './wikis';
import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
+import deleteWikiModal from './components/delete_wiki_modal.vue';
document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
+
+ const deleteWikiButton = document.getElementById('delete-wiki-button');
+
+ if (deleteWikiButton) {
+ Vue.use(Translate);
+
+ const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset;
+ const deleteWikiModalEl = document.getElementById('delete-wiki-modal');
+ const deleteModal = new Vue({ // eslint-disable-line
+ el: deleteWikiModalEl,
+ data: {
+ deleteWikiUrl: '',
+ },
+ render(createElement) {
+ return createElement(deleteWikiModal, {
+ props: {
+ pageTitle,
+ deleteWikiUrl,
+ csrfToken: csrf.token,
+ },
+ });
+ },
+ });
+ }
});
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 8ce938c958b..50d042fef29 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
- date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
+ date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date;
}
@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
-const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+const initColorKey = () =>
+ d3
+ .scaleLinear()
+ .range(['#acd5f2', '#254e77'])
+ .domain([0, 3]);
export default class ActivityCalendar {
- constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
+ constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
- this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
- this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ this.daySizeWithSpace = this.daySize + this.daySpace * 2;
+ this.monthNames = [
+ 'Jan',
+ 'Feb',
+ 'Mar',
+ 'Apr',
+ 'May',
+ 'Jun',
+ 'Jul',
+ 'Aug',
+ 'Sep',
+ 'Oct',
+ 'Nov',
+ 'Dec',
+ ];
this.months = [];
+ this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week
// or if is first object
- if ((day === 0 && i !== 0) || i === 0) {
+ if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group += 1;
}
@@ -109,21 +127,30 @@ export default class ActivityCalendar {
}
renderSvg(container, group) {
- const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
- return d3.select(container)
+ const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+ return d3
+ .select(container)
.append('svg')
- .attr('width', width)
- .attr('height', 167)
- .attr('class', 'contrib-calendar');
+ .attr('width', width)
+ .attr('height', 167)
+ .attr('class', 'contrib-calendar');
+ }
+
+ dayYPos(day) {
+ return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
renderDays() {
- this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
+ this.svg
+ .selectAll('g')
+ .data(this.timestampsTmp)
+ .enter()
+ .append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth();
- const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
+ const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
if (
lastMonth == null ||
@@ -133,86 +160,113 @@ export default class ActivityCalendar {
}
}
});
- return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
+ return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
- .data(stamp => stamp)
- .enter()
- .append('rect')
- .attr('x', '0')
- .attr('y', stamp => this.daySizeWithSpace * stamp.day)
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('fill', stamp => (
- stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
- ))
- .attr('title', stamp => formatTooltipText(stamp))
- .attr('class', 'user-contrib-cell js-tooltip')
- .attr('data-container', 'body')
- .on('click', this.clickDay);
+ .data(stamp => stamp)
+ .enter()
+ .append('rect')
+ .attr('x', '0')
+ .attr('y', stamp => this.dayYPos(stamp.day))
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr(
+ 'fill',
+ stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
+ )
+ .attr('title', stamp => formatTooltipText(stamp))
+ .attr('class', 'user-contrib-cell js-tooltip')
+ .attr('data-container', 'body')
+ .on('click', this.clickDay);
}
renderDayTitles() {
const days = [
{
text: 'M',
- y: 29 + (this.daySizeWithSpace * 1),
- }, {
+ y: 29 + this.dayYPos(1),
+ },
+ {
text: 'W',
- y: 29 + (this.daySizeWithSpace * 3),
- }, {
+ y: 29 + this.dayYPos(3),
+ },
+ {
text: 'F',
- y: 29 + (this.daySizeWithSpace * 5),
+ y: 29 + this.dayYPos(5),
},
];
- this.svg.append('g')
+ this.svg
+ .append('g')
.selectAll('text')
- .data(days)
- .enter()
- .append('text')
- .attr('text-anchor', 'middle')
- .attr('x', 8)
- .attr('y', day => day.y)
- .text(day => day.text)
- .attr('class', 'user-contrib-text');
+ .data(days)
+ .enter()
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', 8)
+ .attr('y', day => day.y)
+ .text(day => day.text)
+ .attr('class', 'user-contrib-text');
}
renderMonths() {
- this.svg.append('g')
+ this.svg
+ .append('g')
.attr('direction', 'ltr')
.selectAll('text')
- .data(this.months)
- .enter()
- .append('text')
- .attr('x', date => date.x)
- .attr('y', 10)
- .attr('class', 'user-contrib-text')
- .text(date => this.monthNames[date.month]);
+ .data(this.months)
+ .enter()
+ .append('text')
+ .attr('x', date => date.x)
+ .attr('y', 10)
+ .attr('class', 'user-contrib-text')
+ .text(date => this.monthNames[date.month]);
}
renderKey() {
- const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
- const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ const keyValues = [
+ 'no contributions',
+ '1-9 contributions',
+ '10-19 contributions',
+ '20-29 contributions',
+ '30+ contributions',
+ ];
+ const keyColors = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
- this.svg.append('g')
- .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
+ this.svg
+ .append('g')
+ .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
- .data(keyColors)
- .enter()
- .append('rect')
- .attr('width', this.daySize)
- .attr('height', this.daySize)
- .attr('x', (color, i) => this.daySizeWithSpace * i)
- .attr('y', 0)
- .attr('fill', color => color)
- .attr('class', 'js-tooltip')
- .attr('title', (color, i) => keyValues[i])
- .attr('data-container', 'body');
+ .data(keyColors)
+ .enter()
+ .append('rect')
+ .attr('width', this.daySize)
+ .attr('height', this.daySize)
+ .attr('x', (color, i) => this.daySizeWithSpace * i)
+ .attr('y', 0)
+ .attr('fill', color => color)
+ .attr('class', 'js-tooltip')
+ .attr('title', (color, i) => keyValues[i])
+ .attr('data-container', 'body');
}
initColor() {
- const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
- return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
+ const colorRange = [
+ '#ededed',
+ this.colorKey(0),
+ this.colorKey(1),
+ this.colorKey(2),
+ this.colorKey(3),
+ ];
+ return d3
+ .scaleThreshold()
+ .domain([0, 10, 20, 30])
+ .range(colorRange);
}
clickDay(stamp) {
@@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML);
- axios.get(this.calendarActivitiesPath, {
- params: {
- date,
- },
- responseType: 'text',
- })
- .then(({ data }) => $('.user-calendar-activities').html(data))
- .catch(() => flash(__('An error occurred while retrieving calendar activity')));
+ axios
+ .get(this.calendarActivitiesPath, {
+ params: {
+ date,
+ },
+ responseType: 'text',
+ })
+ .then(({ data }) => $('.user-calendar-activities').html(data))
+ .catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
$('.user-calendar-activities').html('');
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 124bc2ba710..ca375007ec5 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -181,7 +181,7 @@ export default class UserTabs {
toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading')
- .toggle(status);
+ .toggleClass('hidden', status);
}
setCurrentAction(source) {
@@ -195,6 +195,6 @@ export default class UserTabs {
}
getCurrentAction() {
- return this.$parentEl.find('.nav-links .active a').data('action');
+ return this.$parentEl.find('.nav-links a.active').data('action');
}
}
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 2fd1715ee79..8ffaa52d9e8 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -5,7 +5,6 @@ import PerformanceBarService from '../services/performance_bar_service';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
-import upstreamPerformanceBar from './upstream_performance_bar.vue';
import Flash from '../../flash';
@@ -14,7 +13,6 @@ export default {
detailedMetric,
requestSelector,
simpleMetric,
- upstreamPerformanceBar,
},
props: {
store: {
@@ -128,9 +126,6 @@ export default {
{{ currentRequest.details.host.hostname }}
</span>
</div>
- <upstream-performance-bar
- v-if="initialRequest && currentRequest.details"
- />
<detailed-metric
v-for="metric in $options.detailedMetrics"
:key="metric.metric"
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index 3ed07a4a47d..dd9578a6c7f 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -37,7 +37,7 @@ export default {
<template>
<div
id="peek-request-selector"
- class="pull-right"
+ class="float-right"
>
<select v-model="currentRequestId">
<option
diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
deleted file mode 100644
index 2b5915f381f..00000000000
--- a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
-export default {
- mounted() {
- const upstreamPerformanceBar = document
- .getElementById('peek-view-performance-bar')
- .cloneNode(true);
-
- upstreamPerformanceBar.classList.remove('hidden');
-
- this.$refs.wrapper.appendChild(upstreamPerformanceBar);
- },
-};
-</script>
-<template>
- <div
- id="peek-view-performance-bar-vue"
- class="view"
- ref="wrapper"
- ></div>
-</template>
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index a0ddf36a672..4a98aed7679 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,5 +1,3 @@
-import 'vendor/peek.performance_bar';
-
import Vue from 'vue';
import performanceBarApp from './components/performance_bar_app.vue';
import PerformanceBarStore from './stores/performance_bar_store';
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
deleted file mode 100644
index 0cdffbde05b..00000000000
--- a/app/assets/javascripts/pipelines/components/async_button.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
- /* eslint-disable no-alert */
-
- import eventHub from '../event_hub';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- directives: {
- tooltip,
- },
- components: {
- loadingIcon,
- icon,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- icon: {
- type: String,
- required: true,
- },
- cssClass: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isLoading: false,
- };
- },
- computed: {
- buttonClass() {
- return `btn ${this.cssClass}`;
- },
- },
- created() {
- // We're using eventHub to listen to the modal here instead of
- // using props because it would would make the parent components
- // much more complex to keep track of the loading state of each button
- eventHub.$on('postAction', this.setLoading);
- },
- beforeDestroy() {
- eventHub.$off('postAction', this.setLoading);
- },
- methods: {
- onClick() {
- eventHub.$emit('openConfirmationModal', {
- pipelineId: this.pipelineId,
- endpoint: this.endpoint,
- type: this.type,
- });
- },
- setLoading(endpoint) {
- if (endpoint === this.endpoint) {
- this.isLoading = true;
- }
- },
- },
- };
-</script>
-
-<template>
- <button
- v-tooltip
- type="button"
- @click="onClick"
- :class="buttonClass"
- :title="title"
- :aria-label="title"
- data-container="body"
- data-placement="top"
- :disabled="isLoading">
- <icon
- :name="icon"
- />
- <loading-icon v-if="isLoading" />
- </button>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue
index 8d3d6223d7b..f3219b8291c 100644
--- a/app/assets/javascripts/pipelines/components/blank_state.vue
+++ b/app/assets/javascripts/pipelines/components/blank_state.vue
@@ -17,13 +17,13 @@
<template>
<div class="row empty-state">
- <div class="col-xs-12">
+ <div class="col-12">
<div class="svg-content">
<img :src="svgPath" />
</div>
</div>
- <div class="col-xs-12 text-center">
+ <div class="col-12 text-center">
<div class="text-content">
<h4>{{ message }}</h4>
</div>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index 10ac8c08bed..50c27bed9fd 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -19,13 +19,13 @@
</script>
<template>
<div class="row empty-state js-empty-state">
- <div class="col-xs-12">
+ <div class="col-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
- <div class="col-xs-12">
+ <div class="col-12">
<div class="text-content">
<template v-if="canSetCi">
@@ -34,7 +34,7 @@
</h4>
<p>
- {{ s__(`Pipelines|Continous Integration can help
+ {{ s__(`Pipelines|Continuous Integration can help
catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver
code to your product environment.`) }}
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index e99d949801f..82b4ce083fb 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,11 +1,21 @@
<script>
import $ from 'jquery';
-import tooltip from '../../../vue_shared/directives/tooltip';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { dasherize } from '../../../lib/utils/text_utility';
-import eventHub from '../../event_hub';
+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';
+
/**
- * Renders either a cancel, retry or play icon pointing to the given path.
+ * Renders either a cancel, retry or play icon button and handles the post request
+ *
+ * Used in:
+ * - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
+ * - pipelines table
+ * - pipelines table in merge request page
+ * - pipelines table in commit page
+ * - pipelines detail page in big graph
*/
export default {
components: {
@@ -32,26 +42,41 @@ export default {
required: true,
},
- buttonDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
+ data() {
+ return {
+ isDisabled: false,
+ };
+ },
+
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
- isDisabled() {
- return this.buttonDisabled === this.link;
- },
},
-
methods: {
+ /**
+ * The request should not be handled here.
+ * However due to this component being used in several
+ * different apps it avoids repetition & complexity.
+ *
+ */
onClickAction() {
$(this.$el).tooltip('hide');
- eventHub.$emit('graphAction', this.link);
+
+ this.isDisabled = true;
+
+ axios.post(`${this.link}.json`)
+ .then(() => {
+ this.isDisabled = false;
+ this.$emit('pipelineActionRequestComplete');
+ })
+ .catch(() => {
+ this.isDisabled = false;
+
+ createFlash(__('An error occurred while making the request.'));
+ });
},
},
};
@@ -62,11 +87,12 @@ export default {
@click="onClickAction"
v-tooltip
:title="tooltipText"
- class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
+ class="js-ci-action btn btn-blank
+btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"
:disabled="isDisabled"
>
- <icon :name="actionIcon" />
+ <icon :name="actionIcon"/>
</button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
deleted file mode 100644
index 7c4fd65e36f..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
-
- /**
- * Renders either a cancel, retry or play icon pointing to the given path.
- * TODO: Remove UJS from here and use an async request instead.
- */
- export default {
- components: {
- icon,
- },
-
- directives: {
- tooltip,
- },
- props: {
- tooltipText: {
- type: String,
- required: true,
- },
-
- link: {
- type: String,
- required: true,
- },
-
- actionMethod: {
- type: String,
- required: true,
- },
-
- actionIcon: {
- type: String,
- required: true,
- },
- },
- };
-</script>
-<template>
- <a
- v-tooltip
- :data-method="actionMethod"
- :title="tooltipText"
- :href="link"
- rel="nofollow"
- class="ci-action-icon-wrapper js-ci-status-icon"
- data-container="body"
- aria-label="Job's action"
- >
- <icon :name="actionIcon" />
- </a>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index be213c2ee78..7bfe11ab8cd 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -1,87 +1,93 @@
<script>
- import $ from 'jquery';
- import jobNameComponent from './job_name_component.vue';
- import jobComponent from './job_component.vue';
- import tooltip from '../../../vue_shared/directives/tooltip';
+import $ from 'jquery';
+import JobNameComponent from './job_name_component.vue';
+import JobComponent from './job_component.vue';
+import tooltip from '../../../vue_shared/directives/tooltip';
- /**
- * Renders the dropdown for the pipeline graph.
- *
- * The following object should be provided as `job`:
- *
- * {
- * "id": 4256,
- * "name": "test",
- * "status": {
- * "icon": "icon_status_success",
- * "text": "passed",
- * "label": "passed",
- * "group": "success",
- * "details_path": "/root/ci-mock/builds/4256",
- * "action": {
- * "icon": "retry",
- * "title": "Retry",
- * "path": "/root/ci-mock/builds/4256/retry",
- * "method": "post"
- * }
- * }
- * }
- */
- export default {
- directives: {
- tooltip,
- },
+/**
+ * Renders the dropdown for the pipeline graph.
+ *
+ * The following object should be provided as `job`:
+ *
+ * {
+ * "id": 4256,
+ * "name": "test",
+ * "status": {
+ * "icon": "icon_status_success",
+ * "text": "passed",
+ * "label": "passed",
+ * "group": "success",
+ * "details_path": "/root/ci-mock/builds/4256",
+ * "action": {
+ * "icon": "retry",
+ * "title": "Retry",
+ * "path": "/root/ci-mock/builds/4256/retry",
+ * "method": "post"
+ * }
+ * }
+ * }
+ */
+export default {
+ directives: {
+ tooltip,
+ },
- components: {
- jobComponent,
- jobNameComponent,
- },
+ components: {
+ JobComponent,
+ JobNameComponent,
+ },
- props: {
- job: {
- type: Object,
- required: true,
- },
+ props: {
+ job: {
+ type: Object,
+ required: true,
},
+ },
- computed: {
- tooltipText() {
- return `${this.job.name} - ${this.job.status.label}`;
- },
+ computed: {
+ tooltipText() {
+ return `${this.job.name} - ${this.job.status.label}`;
},
+ },
- mounted() {
- this.stopDropdownClickPropagation();
- },
+ mounted() {
+ this.stopDropdownClickPropagation();
+ },
- methods: {
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
+ methods: {
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name or the action icon
+ * the dropdown should not be closed so we stop propagation
+ * of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
- stopDropdownClickPropagation() {
- $(this.$el
- .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
+ stopDropdownClickPropagation() {
+ $(
+ '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
+ },
+
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
},
- };
+ },
+};
</script>
<template>
- <div class="ci-job-dropdown-container">
+ <div class="ci-job-dropdown-container dropdown">
<button
v-tooltip
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
- :title="tooltipText">
+ :title="tooltipText"
+ >
<job-name-component
:name="job.name"
@@ -98,11 +104,12 @@
<ul>
<li
v-for="(item, i) in job.jobs"
- :key="i">
+ :key="i"
+ >
<job-component
:job="item"
- :is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ac9ce7e47d6..4ec67f6c01b 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -7,7 +7,6 @@ export default {
StageColumnComponent,
LoadingIcon,
},
-
props: {
isLoading: {
type: Boolean,
@@ -17,11 +16,6 @@ export default {
type: Object,
required: true,
},
- actionDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
@@ -52,6 +46,10 @@ export default {
return className;
},
+
+ refreshPipelineGraph() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -75,7 +73,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
- :action-disabled="actionDisabled"
+ @refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index c6e5ae6df41..dc16d395bcb 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -1,6 +1,5 @@
<script>
import ActionComponent from './action_component.vue';
-import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default {
components: {
ActionComponent,
- DropdownActionComponent,
JobNameComponent,
},
-
directives: {
tooltip,
},
@@ -44,26 +41,12 @@ export default {
type: Object,
required: true,
},
-
cssClassJobName: {
type: String,
required: false,
default: '',
},
-
- isDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- actionDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
-
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
@@ -96,6 +79,11 @@ export default {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
+ methods: {
+ pipelineActionRequestComplete() {
+ this.$emit('pipelineActionRequestComplete');
+ },
+ },
};
</script>
<template>
@@ -108,6 +96,7 @@ export default {
:class="cssClassJobName"
data-container="body"
data-html="true"
+ data-boundary="viewport"
class="js-pipeline-graph-job-link"
>
@@ -120,7 +109,7 @@ export default {
<div
v-else
v-tooltip
- class="js-job-component-tooltip"
+ class="js-job-component-tooltip non-details-job-component"
:title="tooltipText"
:class="cssClassJobName"
data-html="true"
@@ -134,19 +123,11 @@ export default {
</div>
<action-component
- v-if="hasAction && !isDropdown"
- :tooltip-text="status.action.title"
- :link="status.action.path"
- :action-icon="status.action.icon"
- :button-disabled="actionDisabled"
- />
-
- <dropdown-action-component
- v-if="hasAction && isDropdown"
+ v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
- :action-method="status.action.method"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
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 f6e6569e15b..f32368947e8 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -29,11 +29,6 @@ export default {
required: false,
default: '',
},
- actionDisabled: {
- type: String,
- required: false,
- default: null,
- },
},
methods: {
@@ -48,6 +43,10 @@ export default {
buildConnnectorClass(index) {
return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
+
+ pipelineActionRequestComplete() {
+ this.$emit('refreshPipelineGraph');
+ },
},
};
</script>
@@ -74,12 +73,13 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
- :action-disabled="actionDisabled"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index ceb4d9ca604..4d965733f95 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -46,7 +46,7 @@
};
</script>
<template>
- <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags">
+ <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
<a
:href="pipeline.path"
class="js-pipeline-url-link">
@@ -69,35 +69,35 @@
<span
v-if="pipeline.flags.latest"
v-tooltip
- class="js-pipeline-url-latest label label-success"
+ class="js-pipeline-url-latest badge badge-success"
title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
v-tooltip
- class="js-pipeline-url-yaml label label-danger"
+ class="js-pipeline-url-yaml badge badge-danger"
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span
v-if="pipeline.flags.failure_reason"
v-tooltip
- class="js-pipeline-url-failure label label-danger"
+ class="js-pipeline-url-failure badge badge-danger"
:title="pipeline.failure_reason">
error
</span>
<a
v-if="pipeline.flags.auto_devops"
tabindex="0"
- class="js-pipeline-url-autodevops label label-info autodevops-badge"
+ class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
v-popover="popoverOptions"
role="button">
Auto DevOps
</a>
<span
v-if="pipeline.flags.stuck"
- class="js-pipeline-url-stuck label label-warning">
+ class="js-pipeline-url-stuck badge badge-warning">
stuck
</span>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
index 3297af7bde4..e9bc3cf14ca 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue
@@ -63,7 +63,7 @@
<loading-icon v-if="isLoading" />
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(action, i) in actions"
:key="i"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
index 1b9e0f917a4..31fcc9dd412 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue
@@ -37,7 +37,7 @@
>
</i>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li
v-for="(artifact, i) in artifacts"
:key="i">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 714aed1333e..4318abe97e0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,7 +1,7 @@
<script>
- import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+ import Modal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
- import pipelinesTableRowComponent from './pipelines_table_row.vue';
+ import PipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub';
/**
@@ -11,8 +11,8 @@
*/
export default {
components: {
- pipelinesTableRowComponent,
- DeprecatedModal,
+ PipelinesTableRowComponent,
+ Modal,
},
props: {
pipelines: {
@@ -37,30 +37,19 @@
return {
pipelineId: '',
endpoint: '',
- type: '',
+ cancelingPipeline: null,
};
},
computed: {
modalTitle() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false) :
- sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
- pipelineId: `'${this.pipelineId}'`,
- }, false);
+ return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
+ pipelineId: `${this.pipelineId}`,
+ }, false);
},
modalText() {
- return this.type === 'stop' ?
- sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false) :
- sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
- pipelineId: `<strong>#${this.pipelineId}</strong>`,
- }, false);
- },
- primaryButtonLabel() {
- return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
+ return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
+ pipelineId: `<strong>#${this.pipelineId}</strong>`,
+ }, false);
},
},
created() {
@@ -73,10 +62,10 @@
setModalData(data) {
this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint;
- this.type = data.type;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
+ this.cancelingPipeline = this.pipelineId;
},
},
};
@@ -119,21 +108,18 @@
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
+ :canceling-pipeline="cancelingPipeline"
/>
- <deprecated-modal
+
+ <modal
id="confirmation-modal"
- :title="modalTitle"
- :text="modalText"
- kind="danger"
- :primary-button-label="primaryButtonLabel"
+ :header-title-text="modalTitle"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('Pipeline|Stop pipeline')"
@submit="onSubmit"
>
- <template
- slot="body"
- slot-scope="props"
- >
- <p v-html="props.text"></p>
- </template>
- </deprecated-modal>
+ <span v-html="modalText"></span>
+ </modal>
+
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 4cbd67e0372..a3c17479e6f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -1,13 +1,15 @@
<script>
- /* eslint-disable no-param-reassign */
- import asyncButtonComponent from './async_button.vue';
- import pipelinesActionsComponent from './pipelines_actions.vue';
- import pipelinesArtifactsComponent from './pipelines_artifacts.vue';
- import ciBadge from '../../vue_shared/components/ci_badge_link.vue';
- import pipelineStage from './stage.vue';
- import pipelineUrl from './pipeline_url.vue';
- import pipelinesTimeago from './time_ago.vue';
- import commitComponent from '../../vue_shared/components/commit.vue';
+ import eventHub from '../event_hub';
+ import PipelinesActionsComponent from './pipelines_actions.vue';
+ import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
+ import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
+ import PipelineStage from './stage.vue';
+ import PipelineUrl from './pipeline_url.vue';
+ import PipelinesTimeago from './time_ago.vue';
+ import CommitComponent from '../../vue_shared/components/commit.vue';
+ import LoadingButton from '../../vue_shared/components/loading_button.vue';
+ import Icon from '../../vue_shared/components/icon.vue';
+ import { PIPELINES_TABLE } from '../constants';
/**
* Pipeline table row.
@@ -16,14 +18,15 @@
*/
export default {
components: {
- asyncButtonComponent,
- pipelinesActionsComponent,
- pipelinesArtifactsComponent,
- commitComponent,
- pipelineStage,
- pipelineUrl,
- ciBadge,
- pipelinesTimeago,
+ PipelinesActionsComponent,
+ PipelinesArtifactsComponent,
+ CommitComponent,
+ PipelineStage,
+ PipelineUrl,
+ CiBadge,
+ PipelinesTimeago,
+ LoadingButton,
+ Icon,
},
props: {
pipeline: {
@@ -43,6 +46,17 @@
type: String,
required: true,
},
+ cancelingPipeline: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ pipelinesTable: PIPELINES_TABLE,
+ data() {
+ return {
+ isRetrying: false,
+ };
},
computed: {
/**
@@ -119,8 +133,10 @@
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
+ // eslint-disable-next-line no-param-reassign
accumulator.ref_url = this.pipeline.ref[prop];
} else {
+ // eslint-disable-next-line no-param-reassign
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
@@ -215,6 +231,23 @@
isChildView() {
return this.viewType === 'child';
},
+
+ isCancelling() {
+ return this.cancelingPipeline === this.pipeline.id;
+ },
+ },
+
+ methods: {
+ handleCancelClick() {
+ eventHub.$emit('openConfirmationModal', {
+ pipelineId: this.pipeline.id,
+ endpoint: this.pipeline.cancel_path,
+ });
+ },
+ handleRetryClick() {
+ this.isRetrying = true;
+ eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+ },
},
};
</script>
@@ -272,6 +305,7 @@
v-for="(stage, index) in pipeline.details.stages"
:key="index">
<pipeline-stage
+ :type="$options.pipelinesTable"
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
@@ -287,7 +321,8 @@
<div
v-if="displayPipelineActions"
- class="table-section section-20 table-button-footer pipeline-actions">
+ class="table-section section-20 table-button-footer pipeline-actions"
+ >
<div class="btn-group table-action-buttons">
<pipelines-actions-component
v-if="pipeline.details.manual_actions.length"
@@ -296,33 +331,31 @@
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
- class="hidden-xs hidden-sm"
+ class="d-none d-sm-none d-md-block"
:artifacts="pipeline.details.artifacts"
/>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.retryable"
- :endpoint="pipeline.retry_path"
- css-class="js-pipelines-retry-button btn-default btn-retry"
- title="Retry"
- icon="repeat"
- :pipeline-id="pipeline.id"
- data-toggle="modal"
- data-target="#confirmation-modal"
- type="retry"
- />
+ @click="handleRetryClick"
+ container-class="js-pipelines-retry-button btn btn-default btn-retry"
+ :loading="isRetrying"
+ :disabled="isRetrying"
+ >
+ <icon name="repeat" />
+ </loading-button>
- <async-button-component
+ <loading-button
v-if="pipeline.flags.cancelable"
- :endpoint="pipeline.cancel_path"
- css-class="js-pipelines-cancel-button btn-remove"
- title="Stop"
- icon="close"
- :pipeline-id="pipeline.id"
+ @click="handleCancelClick"
data-toggle="modal"
data-target="#confirmation-modal"
- type="stop"
- />
+ container-class="js-pipelines-cancel-button btn btn-remove"
+ :loading="isCancelling"
+ :disabled="isCancelling"
+ >
+ <icon name="close" />
+ </loading-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 32cf3dba3c3..f9769815796 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -1,135 +1,157 @@
<script>
-
- /**
- * Renders each stage of the pipeline mini graph.
- *
- * Given the provided endpoint will make a request to
- * fetch the dropdown data when the stage is clicked.
- *
- * Request is made inside this component to make it reusable between:
- * 1. Pipelines main table
- * 2. Pipelines table in commit and Merge request views
- * 3. Merge request widget
- * 4. Commit widget
- */
-
- import $ from 'jquery';
- 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 LoadingIcon from '../../vue_shared/components/loading_icon.vue';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- components: {
- LoadingIcon,
- Icon,
+/**
+ * Renders each stage of the pipeline mini graph.
+ *
+ * Given the provided endpoint will make a request to
+ * fetch the dropdown data when the stage is clicked.
+ *
+ * Request is made inside this component to make it reusable between:
+ * 1. Pipelines main table
+ * 2. Pipelines table in commit and Merge request views
+ * 3. Merge request widget
+ * 4. Commit widget
+ */
+
+import $ from 'jquery';
+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 LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import JobComponent from './graph/job_component.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import { PIPELINES_TABLE } from '../constants';
+
+export default {
+ components: {
+ LoadingIcon,
+ Icon,
+ JobComponent,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ updateDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- props: {
- stage: {
- type: Object,
- required: true,
- },
-
- updateDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
+ type: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ dropdownContent: '',
+ };
+ },
+
+ computed: {
+ dropdownClass() {
+ return this.dropdownContent.length > 0
+ ? 'js-builds-dropdown-container'
+ : 'js-builds-dropdown-loading';
},
- data() {
- return {
- isLoading: false,
- dropdownContent: '',
- };
+ triggerButtonClass() {
+ return `ci-status-icon-${this.stage.status.group}`;
},
- computed: {
- dropdownClass() {
- return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
- },
+ borderlessIcon() {
+ return `${this.stage.status.icon}_borderless`;
+ },
+ },
- triggerButtonClass() {
- return `ci-status-icon-${this.stage.status.group}`;
- },
+ watch: {
+ updateDropdown() {
+ if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
+ this.fetchJobs();
+ }
+ },
+ },
+
+ updated() {
+ if (this.dropdownContent.length > 0) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ onClickStage() {
+ if (!this.isDropdownOpen()) {
+ eventHub.$emit('clickedDropdown');
+ this.isLoading = true;
+ this.fetchJobs();
+ }
+ },
- borderlessIcon() {
- return `${this.stage.status.icon}_borderless`;
- },
+ fetchJobs() {
+ axios
+ .get(this.stage.dropdown_path)
+ .then(({ data }) => {
+ this.dropdownContent = data.latest_statuses;
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.closeDropdown();
+ this.isLoading = false;
+
+ Flash(__('Something went wrong on our end.'));
+ });
},
- watch: {
- updateDropdown() {
- if (this.updateDropdown &&
- this.isDropdownOpen() &&
- !this.isLoading) {
- this.fetchJobs();
- }
- },
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(
+ '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
+ this.$el,
+ ).on('click', e => {
+ e.stopPropagation();
+ });
},
- updated() {
- if (this.dropdownContent.length > 0) {
- this.stopDropdownClickPropagation();
+ closeDropdown() {
+ if (this.isDropdownOpen()) {
+ $(this.$refs.dropdown).dropdown('toggle');
}
},
- methods: {
- onClickStage() {
- if (!this.isDropdownOpen()) {
- eventHub.$emit('clickedDropdown');
- this.isLoading = true;
- this.fetchJobs();
- }
- },
-
- fetchJobs() {
- axios.get(this.stage.dropdown_path)
- .then(({ data }) => {
- this.dropdownContent = data.html;
- this.isLoading = false;
- })
- .catch(() => {
- this.closeDropdown();
- this.isLoading = false;
-
- Flash('Something went wrong on our end.');
- });
- },
-
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
- *
- * Since this component is rendered multiple times per page we need to guarantee we only
- * target the click event of this component.
- */
- stopDropdownClickPropagation() {
- $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item'))
- .on('click', (e) => {
- e.stopPropagation();
- });
- },
-
- closeDropdown() {
- if (this.isDropdownOpen()) {
- $(this.$refs.dropdown).dropdown('toggle');
- }
- },
-
- isDropdownOpen() {
- return this.$el.classList.contains('open');
- },
+ isDropdownOpen() {
+ return this.$el.classList.contains('open');
+ },
+
+ pipelineActionRequestComplete() {
+ if (this.type === PIPELINES_TABLE) {
+ // warn the table to update
+ eventHub.$emit('refreshPipelinesTable');
+ } else {
+ // close the dropdown in mr widget
+ $(this.$refs.dropdown).dropdown('toggle');
+ }
},
- };
+ },
+};
</script>
<template>
@@ -146,6 +168,7 @@
id="stageDropdown"
aria-haspopup="true"
aria-expanded="false"
+ ref="dropdown"
>
<span
@@ -168,7 +191,6 @@
>
<li
- :class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
>
@@ -176,8 +198,17 @@
<ul
v-else
- v-html="dropdownContent"
>
+ <li
+ v-for="job in dropdownContent"
+ :key="job.id"
+ >
+ <job-component
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
</ul>
</li>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue
index cd54d26c9d3..79dbdca4010 100644
--- a/app/assets/javascripts/pipelines/components/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/time_ago.vue
@@ -75,7 +75,7 @@
</p>
<p
- class="finished-at hidden-xs hidden-sm"
+ class="finished-at d-none d-sm-none d-md-block"
v-if="hasFinishedTime"
>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index b384c7500e7..eaa11a84cb9 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -1,2 +1,2 @@
-// eslint-disable-next-line import/prefer-default-export
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
+export const PIPELINES_TABLE = 'PIPELINES_TABLE';
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 6d87f75ae8e..30b1eee186d 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -53,11 +53,15 @@ export default {
});
eventHub.$on('postAction', this.postAction);
+ eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
+ eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
},
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
+ eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
+ eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
},
destroyed() {
this.poll.stop();
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 900eb7855f4..b49a16a87e6 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -25,28 +25,14 @@ export default () => {
data() {
return {
mediator,
- actionDisabled: null,
};
},
- created() {
- eventHub.$on('graphAction', this.postAction);
- },
- beforeDestroy() {
- eventHub.$off('graphAction', this.postAction);
- },
methods: {
- postAction(action) {
- this.actionDisabled = action;
-
- this.mediator.service.postAction(action)
- .then(() => {
- this.mediator.refreshPipeline();
- this.actionDisabled = null;
- })
- .catch(() => {
- this.actionDisabled = null;
- Flash(__('An error occurred while making the request.'));
- });
+ requestRefreshPipelineGraph() {
+ // When an action is clicked
+ // (wether in the dropdown or in the main nodes, we refresh the big graph)
+ this.mediator.refreshPipeline()
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
render(createElement) {
@@ -54,7 +40,9 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
- actionDisabled: this.actionDisabled,
+ },
+ on: {
+ refreshPipelineGraph: this.requestRefreshPipelineGraph,
},
});
},
@@ -79,7 +67,8 @@ export default () => {
},
methods: {
postAction(action) {
- this.mediator.service.postAction(action.path)
+ this.mediator.service
+ .postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index e5de3f69b01..a7a2a7235fd 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -86,7 +86,11 @@ Please update your Git repository remotes as soon as possible.`),
<div class="form-group">
<label :for="$options.inputId">{{ s__('Profiles|Path') }}</label>
<div class="input-group">
- <div class="input-group-addon">{{ rootUrl }}</div>
+ <div class="input-group-prepend">
+ <div class="input-group-text">
+ {{ rootUrl }}
+ </div>
+ </div>
<input
:id="$options.inputId"
class="form-control"
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 6fedd94a6a9..f5cd1c3cc3e 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -4,6 +4,6 @@ export default () => {
$('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false;
- return $('.js-fork-content').toggle();
+ return $('.js-fork-content').toggleClass('hidden');
});
};
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 72f03b02131..a5e68d1b0a0 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -39,7 +39,9 @@ export default class ProjectLabelSubscription {
const $button = $(button);
const originalTitle = $button.attr('data-original-title');
- if (originalTitle) ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
+ if (originalTitle) {
+ ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
+ }
return button;
});
@@ -51,6 +53,6 @@ export default class ProjectLabelSubscription {
const actionRegexp = new RegExp(newStatusVerb, 'i');
const newTitle = originalTitle.replace(actionRegexp, newAction);
- $button.tooltip('hide').attr('data-original-title', newTitle).tooltip('fixTitle');
+ $button.tooltip('hide').attr('data-original-title', newTitle).tooltip('_fixTitle');
}
}
diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js
index 7c95c71e239..a52ac768e57 100644
--- a/app/assets/javascripts/project_visibility.js
+++ b/app/assets/javascripts/project_visibility.js
@@ -7,7 +7,7 @@ function setVisibilityOptions(namespaceSelector) {
const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
- document.querySelectorAll('.visibility-level-setting .radio').forEach((option) => {
+ document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => {
const optionInput = option.querySelector('input[type=radio]');
const optionValue = optionInput ? optionInput.value : 0;
const optionTitle = option.querySelector('.option-title');
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 93603dfc14d..888b1d6ce33 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -66,8 +66,8 @@ const bindEvents = () => {
.on('click', (e) => { e.preventDefault(); })
.popover({
title: $pushNewProjectTipTrigger.data('title'),
- placement: 'auto bottom',
- html: 'true',
+ placement: 'bottom',
+ html: true,
content: $('.push-new-project-tip-template').html(),
})
.on('shown.bs.popover', () => {
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
index 34a60dd574b..0bbd8a41753 100644
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ b/app/assets/javascripts/projects_dropdown/components/app.vue
@@ -100,9 +100,10 @@ export default {
fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery;
this.toggleLoader(true);
- this.service.getSearchedProjects(this.searchQuery)
+ this.service
+ .getSearchedProjects(this.searchQuery)
.then(res => res.json())
- .then((results) => {
+ .then(results => {
this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results);
})
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
index 0c46ed184be..7fcd62dcdb8 100644
--- a/app/assets/javascripts/projects_dropdown/components/search.vue
+++ b/app/assets/javascripts/projects_dropdown/components/search.vue
@@ -46,7 +46,7 @@
<template>
<div
- class="search-input-container hidden-xs"
+ class="search-input-container d-none d-sm-block"
>
<input
type="search"
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
index 7231f520933..ed1c3deead2 100644
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js
@@ -50,7 +50,7 @@ export default class ProjectsService {
} else {
// Check if project is already present in frequents list
// When found, update metadata of it.
- storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
+ storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
if (projectItem.id === project.id) {
matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
@@ -104,13 +104,17 @@ export default class ProjectsService {
return [];
}
- if (bp.getBreakpointSize() === 'sm' ||
- bp.getBreakpointSize() === 'xs') {
+ if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
}
- const frequentProjects = storedFrequentProjects
- .filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
+ const frequentProjects = storedFrequentProjects.filter(
+ project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
+ );
+
+ if (!frequentProjects || frequentProjects.length === 0) {
+ return [];
+ }
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 0a60f4845b2..1a75fdd75db 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -31,7 +31,7 @@ export default class PrometheusMetrics {
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
- const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body');
+ const $currentPanelBody = $toggleBtn.closest('.card').find('.card-body');
$currentPanelBody.toggleClass('hidden');
if ($toggleBtn.hasClass('fa-caret-down')) {
$toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right');
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index a03180e80e6..2fc3778820b 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -28,11 +28,6 @@
isOpen: false,
};
},
- computed: {
- clipboardText() {
- return `docker pull ${this.repo.location}`;
- },
- },
methods: {
...mapActions([
'fetchRepos',
@@ -84,12 +79,12 @@
<clipboard-button
v-if="repo.location"
- :text="clipboardText"
+ :text="repo.location"
:title="repo.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
- <div class="controls hidden-xs pull-right">
+ <div class="controls d-none d-sm-block float-right">
<button
v-if="repo.canDelete"
type="button"
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index ee4eb3581f3..e4a4b3bb129 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -56,10 +56,6 @@
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
},
- clipboardText(text) {
- return `docker pull ${text}`;
- },
-
showError(message) {
Flash(errorMessages[message]);
},
@@ -89,7 +85,7 @@
<clipboard-button
v-if="item.location"
:title="item.location"
- :text="clipboardText(item.location)"
+ :text="item.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</td>
@@ -111,14 +107,20 @@
</td>
<td>
- {{ timeFormated(item.createdAt) }}
+ <span
+ v-tooltip
+ :title="tooltipTitle(item.createdAt)"
+ data-placement="bottom"
+ >
+ {{ timeFormated(item.createdAt) }}
+ </span>
</td>
<td class="content">
<button
v-if="item.canDelete"
type="button"
- class="js-delete-registry btn btn-danger hidden-xs pull-right"
+ class="js-delete-registry btn btn-danger d-none d-sm-block float-right"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
data-container="body"
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 795b39bb3dc..593a43c7cc1 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js
index 588f479c492..f4923512578 100644
--- a/app/assets/javascripts/registry/stores/getters.js
+++ b/app/assets/javascripts/registry/stores/getters.js
@@ -1,2 +1,5 @@
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 2088a49590a..2afcf4626b8 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- var $allGutterToggleIcons, $this, $thisIcon;
+ var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
- $thisIcon = $this.find('i');
+ isExpanded = $this.find('i').hasClass('fa-angle-double-right');
+ tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
- if ($thisIcon.hasClass('fa-angle-double-right')) {
+
+ 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');
@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
+
+ $this.attr('data-original-title', tooltipLabel);
+
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
@@ -102,7 +108,7 @@ Sidebar.prototype.todoUpdateDone = function(data) {
.attr('title', $el.data(`${attrPrefix}Text`));
if ($el.hasClass('has-tooltip')) {
- $el.tooltip('fixTitle');
+ $el.tooltip('_fixTitle');
}
if ($el.data(`${attrPrefix}Icon`)) {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e31e067033f..99c71d6524a 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -85,6 +85,7 @@ export default class Shortcuts {
if ($modal.length) {
$modal.modal('toggle');
+ return null;
}
return axios.get(gon.shortcuts_path, {
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index a4d10850471..78f7353eb0d 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -7,7 +7,7 @@ export default class ShortcutsNavigation extends Shortcuts {
super();
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
- Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
+ Mousetrap.bind('g v', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
@@ -16,9 +16,10 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
- Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
+ Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
+ Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 5eeb2a41bae..284a258d3c9 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -41,7 +41,7 @@ export default {
</i>
<a
v-if="editable"
- class="js-sidebar-dropdown-toggle edit-link pull-right"
+ class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
>
{{ __('Edit') }}
@@ -49,7 +49,7 @@ export default {
<a
v-if="showToggle"
aria-label="Toggle sidebar"
- class="gutter-toggle pull-right js-sidebar-toggle"
+ class="gutter-toggle float-right js-sidebar-toggle"
href="#"
role="button"
>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 1e7f46454bf..5a374d84796 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,6 +1,12 @@
<script>
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
export default {
name: 'Assignees',
+ directives: {
+ tooltip,
+ },
props: {
rootPath: {
type: String,
@@ -14,6 +20,11 @@ export default {
type: Boolean,
required: true,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`);
}
+ if (!this.users.length) {
+ const emptyTooltipLabel = this.issuableType === 'issue' ?
+ __('Assignee(s)') : __('Assignee');
+ names.push(emptyTooltipLabel);
+ }
+
return names.join(', ');
},
sidebarAvatarCounter() {
@@ -109,9 +126,11 @@ export default {
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ v-tooltip
data-container="body"
data-placement="left"
+ data-boundary="viewport"
:title="collapsedTooltipTitle"
>
<i
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 3c6b9c27814..b04a2eff798 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,9 +1,9 @@
<script>
-import Flash from '../../../flash';
+import Flash from '~/flash';
+import eventHub from '~/sidebar/event_hub';
+import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
-import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
@@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
+ issuableType: {
+ type: String,
+ require: true,
+ default: 'issue',
+ },
},
data() {
return {
@@ -90,6 +95,7 @@ export default {
:users="store.assignees"
:editable="store.editable"
@assign-self="assignSelf"
+ :issuable-type="issuableType"
/>
</div>
</template>
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 ceb02309959..3f6e2f05396 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,15 +1,19 @@
<script>
-import Flash from '../../../flash';
+import { __ } from '~/locale';
+import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import Icon from '../../../vue_shared/components/icon.vue';
-import { __ } from '../../../locale';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+ directives: {
+ tooltip,
+ },
props: {
isConfidential: {
required: true,
@@ -33,6 +37,9 @@ export default {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
+ tooltipLabel() {
+ return this.isConfidential ? __('Confidential') : __('Not confidential');
+ },
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
@@ -65,6 +72,11 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="tooltipLabel"
>
<icon
:name="confidentialityIcon"
@@ -75,7 +87,7 @@ export default {
{{ __('Confidentiality') }}
<a
v-if="isEditable"
- class="pull-right confidential-edit"
+ class="float-right confidential-edit"
href="#"
@click.prevent="toggleForm"
>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index 3783f71a848..4165aa19acf 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -32,7 +32,7 @@ export default {
</script>
<template>
- <div class="dropdown open">
+ <div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
<p
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index e1e4715826a..d392977e5e2 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -41,7 +41,7 @@ export default {
</script>
<template>
- <div class="dropdown open">
+ <div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<p
class="text"
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 e4893451af3..fb69c741dcd 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,15 +1,22 @@
<script>
+import { __ } from '~/locale';
import Flash from '~/flash';
+import tooltip from '~/vue_shared/directives/tooltip';
+import issuableMixin from '~/vue_shared/mixins/issuable';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
-import issuableMixin from '../../../vue_shared/mixins/issuable';
-import Icon from '../../../vue_shared/components/icon.vue';
-import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
+
+ directives: {
+ tooltip,
+ },
+
mixins: [issuableMixin],
props: {
@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
+
+ tooltipLabel() {
+ return this.isLocked ? __('Locked') : __('Unlocked');
+ },
},
created() {
@@ -85,6 +96,11 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="tooltipLabel"
>
<icon
:name="lockIcon"
@@ -97,7 +113,7 @@ export default {
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button
v-if="isEditable"
- class="pull-right lock-edit"
+ class="float-right lock-edit"
type="button"
@click.prevent="toggleForm"
>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 006a6d2905d..0a945fc7fd5 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -1,9 +1,13 @@
<script>
- import { __, n__, sprintf } from '../../../locale';
- import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
- 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 loadingIcon from '~/vue_shared/components/loading_icon.vue';
+ import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
+ directives: {
+ tooltip,
+ },
components: {
loadingIcon,
userAvatarImage,
@@ -66,13 +70,24 @@
toggleMoreParticipants() {
this.isShowingMoreParticipants = !this.isShowingMoreParticipants;
},
+ onClickCollapsedIcon() {
+ this.$emit('toggleSidebar');
+ },
},
};
</script>
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="participantLabel"
+ @click="onClickCollapsedIcon"
+ >
<i
class="fa fa-users"
aria-hidden="true"
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 3e8cc7a6630..385717e7c1e 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -1,6 +1,5 @@
<script>
import Store from '../../stores/sidebar_store';
-import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
@@ -20,12 +19,6 @@ export default {
store: new Store(),
};
},
- created() {
- eventHub.$on('toggleSubscription', this.onToggleSubscription);
- },
- beforeDestroy() {
- eventHub.$off('toggleSubscription', this.onToggleSubscription);
- },
methods: {
onToggleSubscription() {
this.mediator.toggleSubscription()
@@ -42,6 +35,7 @@ export default {
<subscriptions
:loading="store.isFetching.subscriptions"
:subscribed="store.subscribed"
+ @toggleSubscription="onToggleSubscription"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index d69d100a26c..6745c1aafff 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -47,8 +47,25 @@
},
},
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');
},
},
};
@@ -56,12 +73,16 @@
<template>
<div>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ @click="onClickCollapsedIcon"
+ >
<span
v-tooltip
:title="notificationTooltip"
data-container="body"
data-placement="left"
+ data-boundary="viewport"
>
<icon
:name="notificationIcon"
@@ -71,12 +92,12 @@
/>
</span>
</div>
- <span class="issuable-header-text hide-collapsed pull-left">
+ <span class="issuable-header-text hide-collapsed float-left">
{{ __('Notifications') }}
</span>
<toggle-button
ref="toggleButton"
- class="pull-right hide-collapsed js-issuable-subscribe-button"
+ class="float-right hide-collapsed js-issuable-subscribe-button"
:is-loading="showLoadingState"
:value="subscribed"
@change="toggleSubscription"
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 3b86f1145d1..209af1ce152 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,12 +1,17 @@
<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import { abbreviateTime } from '../../../lib/utils/pretty_time';
+ 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';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
+ directives: {
+ tooltip,
+ },
props: {
showComparisonState: {
type: Boolean,
@@ -79,6 +84,21 @@
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;
+ },
},
methods: {
abbreviateTime(timeStr) {
@@ -89,7 +109,14 @@
</script>
<template>
- <div class="sidebar-collapsed-icon">
+ <div
+ class="sidebar-collapsed-icon"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ :title="tooltipText"
+ >
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">
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 82c4562f9a9..6f79310b1cc 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -71,7 +71,7 @@ export default {
</div>
</div>
<div class="compare-display-container">
- <div class="compare-display pull-left">
+ <div class="compare-display float-left">
<span class="compare-label">
{{ s__('TimeTracking|Spent') }}
</span>
@@ -79,7 +79,7 @@ export default {
{{ timeSpentHumanReadable }}
</span>
</div>
- <div class="compare-display estimated pull-right">
+ <div class="compare-display estimated float-right">
<span class="compare-label">
{{ s__('TimeTrackingEstimated|Est') }}
</span>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
deleted file mode 100644
index 38da76c6771..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- name: 'time-tracking-no-tracking-pane',
- template: `
- <div class="time-tracking-no-tracking-pane">
- <span class="no-value">
- {{ __('No estimate or time spent') }}
- </span>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
new file mode 100644
index 00000000000..9228184df5b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue
@@ -0,0 +1,13 @@
+<script>
+export default {
+ name: 'TimeTrackingNoTrackingPane',
+};
+</script>
+
+<template>
+ <div class="time-tracking-no-tracking-pane">
+ <span class="no-value">
+ {{ __('No estimate or time spent') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
deleted file mode 100644
index 5626cccc022..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import $ from 'jquery';
-import _ from 'underscore';
-
-import '~/smart_interval';
-
-import IssuableTimeTracker from './time_tracker.vue';
-
-import Store from '../../stores/sidebar_store';
-import Mediator from '../../sidebar_mediator';
-import eventHub from '../../event_hub';
-
-export default {
- data() {
- return {
- mediator: new Mediator(),
- store: new Store(),
- };
- },
- components: {
- IssuableTimeTracker,
- },
- methods: {
- listenForQuickActions() {
- $(document).on('ajax:success', '.gfm-form', this.quickActionListened);
- eventHub.$on('timeTrackingUpdated', (data) => {
- this.quickActionListened(null, data);
- });
- },
- quickActionListened(e, data) {
- const subscribedCommands = ['spend_time', 'time_estimate'];
- let changedCommands;
- if (data !== undefined) {
- changedCommands = data.commands_changes
- ? Object.keys(data.commands_changes)
- : [];
- } else {
- changedCommands = [];
- }
- if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
- this.mediator.fetch();
- }
- },
- },
- mounted() {
- this.listenForQuickActions();
- },
- template: `
- <div class="block">
- <issuable-time-tracker
- :time_estimate="store.timeEstimate"
- :time_spent="store.totalTimeSpent"
- :human_time_estimate="store.humanTimeEstimate"
- :human_time_spent="store.humanTotalTimeSpent"
- :rootPath="store.rootPath"
- />
- </div>
- `,
-};
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
new file mode 100644
index 00000000000..2e1d6e9643a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -0,0 +1,61 @@
+<script>
+import $ from 'jquery';
+import _ from 'underscore';
+
+import '~/smart_interval';
+
+import IssuableTimeTracker from './time_tracker.vue';
+
+import Store from '../../stores/sidebar_store';
+import Mediator from '../../sidebar_mediator';
+import eventHub from '../../event_hub';
+
+export default {
+ components: {
+ IssuableTimeTracker,
+ },
+ data() {
+ return {
+ mediator: new Mediator(),
+ store: new Store(),
+ };
+ },
+ mounted() {
+ this.listenForQuickActions();
+ },
+ methods: {
+ listenForQuickActions() {
+ $(document).on('ajax:success', '.gfm-form', this.quickActionListened);
+ eventHub.$on('timeTrackingUpdated', (data) => {
+ this.quickActionListened(null, data);
+ });
+ },
+ quickActionListened(e, data) {
+ const subscribedCommands = ['spend_time', 'time_estimate'];
+ let changedCommands;
+ if (data !== undefined) {
+ changedCommands = data.commands_changes
+ ? Object.keys(data.commands_changes)
+ : [];
+ } else {
+ changedCommands = [];
+ }
+ if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
+ this.mediator.fetch();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block">
+ <issuable-time-tracker
+ :time_estimate="store.timeEstimate"
+ :time_spent="store.totalTimeSpent"
+ :human_time_estimate="store.humanTimeEstimate"
+ :human_time_spent="store.humanTotalTimeSpent"
+ :root-path="store.rootPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
deleted file mode 100644
index bf987562647..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default {
- name: 'time-tracking-spent-only-pane',
- props: {
- timeSpentHumanReadable: {
- type: String,
- required: true,
- },
- },
- template: `
- <div class="time-tracking-spend-only-pane">
- <span class="bold">Spent:</span>
- {{ timeSpentHumanReadable }}
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
new file mode 100644
index 00000000000..59cd99f8f14
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -0,0 +1,18 @@
+<script>
+export default {
+ name: 'TimeTrackingSpentOnlyPane',
+ props: {
+ timeSpentHumanReadable: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="time-tracking-spend-only-pane">
+ <span class="bold">Spent:</span>
+ {{ timeSpentHumanReadable }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 71dca498b3d..7d56d2fa5ee 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,8 +1,8 @@
<script>
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
-import timeTrackingSpentOnlyPane from './spent_only_pane';
-import timeTrackingNoTrackingPane from './no_tracking_pane';
+import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
+import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
@@ -13,8 +13,8 @@ export default {
components: {
TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane,
- 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
- 'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane,
+ TimeTrackingSpentOnlyPane,
+ TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane,
TimeTrackingHelpState,
},
@@ -116,7 +116,7 @@ export default {
<div class="title hide-collapsed">
{{ __('Time tracking') }}
<div
- class="help-button pull-right"
+ class="help-button float-right"
v-if="!showHelpState"
@click="toggleHelpState(true)"
>
@@ -127,7 +127,7 @@ export default {
</i>
</div>
<div
- class="close-help-button pull-right"
+ class="close-help-button float-right"
v-if="showHelpState"
@click="toggleHelpState(false)"
>
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 1eadebc7004..b267422cd97 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import _ from 'underscore';
function isValidProjectId(id) {
return id > 0;
@@ -43,7 +44,7 @@ class SidebarMoveIssue {
renderRow: project => `
<li>
<a href="#" class="js-move-issue-dropdown-item">
- ${project.name_with_namespace}
+ ${_.escape(project.name_with_namespace)}
</a>
</li>
`,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9f5d852260e..3086e7d0fc9 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
+import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
+ issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index 97d5cf96bcb..96dfff77859 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -15,7 +15,7 @@ export default class UserCallout {
init() {
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
- $('.js-close-callout').on('click', e => this.dismissCallout(e));
+ this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
@@ -23,12 +23,15 @@ export default class UserCallout {
const $currentTarget = $(e.currentTarget);
if (this.options.setCalloutPerProject) {
- Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') });
+ Cookies.set(this.cookieName, 'true', {
+ expires: 365,
+ path: this.userCalloutBody.data('projectPath'),
+ });
} else {
Cookies.set(this.cookieName, 'true', { expires: 365 });
}
- if ($currentTarget.hasClass('close')) {
+ if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) {
this.userCalloutBody.remove();
}
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 520a0b3f424..cd954f75613 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data)
.then(({ data }) => {
- var user;
+ var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
+ tooltipTitle = _.escape(user.name);
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
+ tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(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 7bef2e97349..1608858da22 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -109,12 +109,12 @@ export default {
rel="noopener noreferrer nofollow"
class="deploy-link js-deploy-url"
>
+ {{ deployment.external_url_formatted }}
<i
class="fa fa-external-link"
aria-hidden="true"
>
</i>
- {{ deployment.external_url_formatted }}
</a>
</template>
<span
@@ -127,7 +127,7 @@ export default {
</span>
<loading-button
v-if="deployment.stop_url"
- container-class="btn btn-default btn-xs prepend-left-default"
+ container-class="btn btn-default btn-sm prepend-left-default"
label="Stop environment"
:loading="isStopping"
@click="stopEnvironment"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index cb6e9858736..8338fde61c7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -2,7 +2,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
export default {
- name: 'MRWidgetAuthor',
+ name: 'MrWidgetAuthor',
directives: {
tooltip,
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
index 8f1fd809a81..644e4b7d81a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue
@@ -1,10 +1,10 @@
<script>
- import mrWidgetAuthor from './mr_widget_author.vue';
+ import MrWidgetAuthor from './mr_widget_author.vue';
export default {
name: 'MRWidgetAuthorTime',
components: {
- mrWidgetAuthor,
+ MrWidgetAuthor,
},
props: {
actionText: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 18ee4c62bf1..51a0fda6555 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -131,7 +131,7 @@ export default {
aria-hidden="true">
</i>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu dropdown-menu-right">
<li>
<a
class="js-download-email-patches"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
deleted file mode 100644
index f0298f732ea..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-<script>
- export default {
- name: 'MRWidgetMaintainerEdit',
- props: {
- maintainerEditAllowed: {
- type: Boolean,
- default: false,
- required: false,
- },
- },
- };
-</script>
-
-<template>
- <section class="mr-info-list mr-links">
- <p v-if="maintainerEditAllowed">
- {{ s__("mrWidget|Allows edits from maintainers") }}
- </p>
- </section>
-</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 ebaf2b972eb..b404a592234 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
@@ -42,7 +42,7 @@
@click="refreshWidget"
:disabled="isRefreshing"
type="button"
- class="btn btn-xs btn-default"
+ class="btn btn-sm btn-default"
>
<loading-icon
v-if="isRefreshing"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index dad4b0fe49d..5c1500ab801 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -43,13 +43,13 @@ To merge this request, first rebase locally.`) }}
<a
v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
- class="js-resolve-conflicts-button btn btn-default btn-xs"
+ class="js-resolve-conflicts-button btn btn-default btn-sm"
>
{{ s__("mrWidget|Resolve conflicts") }}
</a>
<button
v-if="mr.canMerge"
- class="js-merge-locally-button btn btn-default btn-xs"
+ class="js-merge-locally-button btn btn-default btn-sm"
data-toggle="modal"
data-target="#modal_merge_info"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 602b68ea572..05fecd4de35 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -1,66 +1,70 @@
<script>
- import { n__ } from '~/locale';
- import statusIcon from '../mr_widget_status_icon.vue';
- import eventHub from '../../event_hub';
+import { n__ } from '~/locale';
+import statusIcon from '../mr_widget_status_icon.vue';
+import eventHub from '../../event_hub';
- export default {
- name: 'MRWidgetFailedToMerge',
+export default {
+ name: 'MRWidgetFailedToMerge',
- components: {
- statusIcon,
- },
+ components: {
+ statusIcon,
+ },
- props: {
- mr: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ default: () => ({}),
},
+ },
- data() {
- return {
- timer: 10,
- isRefreshing: false,
- };
- },
+ data() {
+ return {
+ timer: 10,
+ isRefreshing: false,
+ intervalId: null,
+ };
+ },
- computed: {
- timerText() {
- return n__(
- 'Refreshing in a second to show the updated status...',
- 'Refreshing in %d seconds to show the updated status...',
- this.timer,
- );
- },
+ computed: {
+ timerText() {
+ return n__(
+ 'Refreshing in a second to show the updated status...',
+ 'Refreshing in %d seconds to show the updated status...',
+ this.timer,
+ );
},
+ },
- mounted() {
- setInterval(() => {
- this.updateTimer();
- }, 1000);
- },
+ mounted() {
+ this.intervalId = setInterval(this.updateTimer, 1000);
+ },
- created() {
- eventHub.$emit('DisablePolling');
- },
+ created() {
+ eventHub.$emit('DisablePolling');
+ },
- methods: {
- refresh() {
- this.isRefreshing = true;
- eventHub.$emit('MRWidgetUpdateRequested');
- eventHub.$emit('EnablePolling');
- },
- updateTimer() {
- this.timer = this.timer - 1;
+ beforeDestroy() {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ },
- if (this.timer === 0) {
- this.refresh();
- }
- },
+ methods: {
+ refresh() {
+ this.isRefreshing = true;
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('EnablePolling');
},
+ updateTimer() {
+ this.timer = this.timer - 1;
- };
+ if (this.timer === 0) {
+ this.refresh();
+ }
+ },
+ },
+};
</script>
<template>
<div class="mr-widget-body media">
@@ -94,7 +98,7 @@
</span>
<button
@click="refresh"
- class="btn btn-default btn-xs js-refresh-button"
+ class="btn btn-default btn-sm js-refresh-button"
type="button"
>
{{ s__("mrWidget|Refresh now") }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 7ff7fc7988a..e8352c362d6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -1,13 +1,13 @@
<script>
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
- import mrWidgetAuthor from '../../components/mr_widget_author.vue';
+ import MrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
components: {
- mrWidgetAuthor,
+ MrWidgetAuthor,
statusIcon,
},
props: {
@@ -94,7 +94,7 @@
:disabled="isCancellingAutoMerge"
role="button"
href="#"
- class="btn btn-xs btn-default js-cancel-auto-merge">
+ class="btn btn-sm btn-default js-cancel-auto-merge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
@@ -129,7 +129,7 @@
:disabled="isRemovingSourceBranch"
@click.prevent="removeSourceBranch"
role="button"
- class="btn btn-xs btn-default js-remove-source-branch"
+ class="btn btn-sm btn-default js-remove-source-branch"
href="#"
>
<i
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 c1618bc6ea0..8c7e0664559 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
@@ -3,6 +3,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
+ 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';
@@ -16,6 +17,7 @@
mrWidgetAuthorTime,
loadingIcon,
statusIcon,
+ ClipboardButton,
},
props: {
mr: {
@@ -116,7 +118,7 @@
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
- class="btn btn-close btn-xs"
+ class="btn btn-close btn-sm"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
@@ -127,7 +129,7 @@
<a
v-else-if="mr.revertInForkPath"
v-tooltip
- class="btn btn-close btn-xs"
+ class="btn btn-close btn-sm"
data-method="post"
:href="mr.revertInForkPath"
:title="revertTitle"
@@ -137,7 +139,7 @@
<a
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
- class="btn btn-default btn-xs"
+ class="btn btn-default btn-sm"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
@@ -148,7 +150,7 @@
<a
v-else-if="mr.cherryPickInForkPath"
v-tooltip
- class="btn btn-default btn-xs"
+ class="btn btn-default btn-sm"
data-method="post"
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
@@ -162,6 +164,18 @@
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
+ with
+ <a
+ :href="mr.mergeCommitPath"
+ class="commit-sha js-mr-merged-commit-sha"
+ >
+ {{ mr.shortMergeCommitSha }}
+ </a>
+ <clipboard-button
+ :title="__('Copy commit SHA to clipboard')"
+ :text="mr.shortMergeCommitSha"
+ css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha"
+ />
</p>
<p v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }}
@@ -175,7 +189,7 @@
@click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
- class="btn btn-xs btn-default js-remove-branch-button"
+ class="btn btn-sm btn-default js-remove-branch-button"
>
{{ s__("mrWidget|Remove Source Branch") }}
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
deleted file mode 100644
index bf8628d18a6..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-The squash-before-merge button is EE only, but it's located right in the middle
-of the readyToMerge state component template.
-
-If we didn't declare this component in CE, we'd need to maintain a separate copy
-of the readyToMergeState template in EE, which is pretty big and likely to change.
-
-Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
-In EE, the configuration extends this object to add a functioning squash-before-merge
-button.
-*/
-
-export default {
- template: '',
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
new file mode 100644
index 00000000000..926a3172412
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue
@@ -0,0 +1,15 @@
+/*
+The squash-before-merge button is EE only, but it's located right in the middle
+of the readyToMerge state component template.
+
+If we didn't declare this component in CE, we'd need to maintain a separate copy
+of the readyToMergeState template in EE, which is pretty big and likely to change.
+
+Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
+In EE, the configuration extends this object to add a functioning squash-before-merge
+button.
+*/
+
+<script>
+ export default {};
+</script>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
deleted file mode 100644
index 44e1a616a19..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import $ from 'jquery';
-import statusIcon from '../mr_widget_status_icon.vue';
-import tooltip from '../../../vue_shared/directives/tooltip';
-import eventHub from '../../event_hub';
-
-export default {
- name: 'MRWidgetWIP',
- props: {
- mr: { type: Object, required: true },
- service: { type: Object, required: true },
- },
- directives: {
- tooltip,
- },
- data() {
- return {
- isMakingRequest: false,
- };
- },
- components: {
- statusIcon,
- },
- methods: {
- removeWIP() {
- this.isMakingRequest = true;
- this.service.removeWIP()
- .then(res => res.data)
- .then((data) => {
- eventHub.$emit('UpdateWidgetData', data);
- new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
- $('.merge-request .detail-page-description .title').text(this.mr.title);
- })
- .catch(() => {
- this.isMakingRequest = false;
- new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line
- });
- },
- },
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
- <div class="media-body space-children">
- <span class="bold">
- This is a Work in Progress
- <i
- v-tooltip
- class="fa fa-question-circle"
- title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
- aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
- </i>
- </span>
- <button
- v-if="mr.removeWIPPath"
- @click="removeWIP"
- :disabled="isMakingRequest"
- type="button"
- class="btn btn-default btn-xs js-remove-wip">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Resolve WIP status
- </button>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 3d9161f6926..086dbabe77e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -18,10 +18,10 @@ export default {
<template>
<div class="mr-widget-body mr-widget-empty-state">
<div class="row">
- <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
+ <div class="artwork col-md-5 order-md-last col-12 text-center">
<span v-html="emptyStateSVG"></span>
</div>
- <div class="text col-sm-7 col-sm-pull-5 col-xs-12">
+ <div class="text col-md-7 order-md-first col-12">
<span>
Merge requests are a place to propose changes you have made to a project
and discuss those changes with others.
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 0264625a526..1d1c8ebc179 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
@@ -312,7 +312,7 @@ export default {
v-else
@click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
- class="js-modify-commit-message-button btn btn-default btn-xs"
+ class="js-modify-commit-message-button btn btn-default btn-sm"
type="button">
Modify commit message
</button>
@@ -329,7 +329,7 @@ export default {
class="prepend-top-default commit-message-editor">
<div class="form-group clearfix">
<label
- class="control-label"
+ class="col-form-label"
for="commit-message">
Commit message
</label>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index a1f7e696795..8ea3f22ecc2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -28,7 +28,7 @@ export default {
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-xs js-create-issue"
+ class="btn btn-default btn-sm js-create-issue"
>
{{ s__("mrWidget|Create an issue to resolve them later") }}
</a>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
new file mode 100644
index 00000000000..fe2608e8212
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -0,0 +1,76 @@
+<script>
+import $ from 'jquery';
+import statusIcon from '../mr_widget_status_icon.vue';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import eventHub from '../../event_hub';
+
+export default {
+ name: 'WorkInProgress',
+ components: {
+ statusIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ mr: { type: Object, required: true },
+ service: { type: Object, required: true },
+ },
+ data() {
+ return {
+ isMakingRequest: false,
+ };
+ },
+ methods: {
+ removeWIP() {
+ this.isMakingRequest = true;
+ this.service.removeWIP()
+ .then(res => res.data)
+ .then((data) => {
+ eventHub.$emit('UpdateWidgetData', data);
+ new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
+ $('.merge-request .detail-page-description .title').text(this.mr.title);
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="Boolean(mr.removeWIPPath)"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ This is a Work in Progress
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ title="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged"
+ aria-label="When this merge request is ready,
+ remove the WIP: prefix from the title to allow it to be merged">
+ </i>
+ </span>
+ <button
+ v-if="mr.removeWIPPath"
+ @click="removeWIP"
+ :disabled="isMakingRequest"
+ type="button"
+ class="btn btn-default btn-xs js-remove-wip">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true">
+ </i>
+ Resolve WIP status
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 3b5c973e4a0..15097fa2a3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -15,13 +15,12 @@ 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 WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.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 WipState } from './components/states/mr_widget_wip';
+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';
@@ -41,8 +40,8 @@ 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/mr_widget_squash_before_merge';
+export { default as SquashBeforeMerge } from './components/states/mr_widget_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';
+export { default as mrWidgetOptions } from './mr_widget_options.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
deleted file mode 100644
index 0be5d9e5a55..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ /dev/null
@@ -1,277 +0,0 @@
-import Project from '~/pages/projects/project';
-import SmartInterval from '~/smart_interval';
-import Flash from '../flash';
-import {
- WidgetHeader,
- WidgetMergeHelp,
- WidgetPipeline,
- Deployment,
- WidgetMaintainerEdit,
- WidgetRelatedLinks,
- MergedState,
- ClosedState,
- MergingState,
- RebaseState,
- WipState,
- ArchivedState,
- ConflictsState,
- NothingToMergeState,
- MissingBranchState,
- NotAllowedState,
- ReadyToMergeState,
- ShaMismatchState,
- UnresolvedDiscussionsState,
- PipelineBlockedState,
- PipelineFailedState,
- FailedToMerge,
- MergeWhenPipelineSucceedsState,
- AutoMergeFailed,
- CheckingState,
- MRWidgetStore,
- MRWidgetService,
- eventHub,
- stateMaps,
- SquashBeforeMerge,
- notify,
- SourceBranchRemovalStatus,
-} from './dependencies';
-import { setFavicon } from '../lib/utils/common_utils';
-
-export default {
- el: '#js-vue-mr-widget',
- name: 'MRWidget',
- props: {
- mrData: {
- type: Object,
- required: false,
- },
- },
- data() {
- const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData);
- const service = this.createService(store);
- return {
- mr: store,
- service,
- };
- },
- computed: {
- componentName() {
- return stateMaps.stateToComponentMap[this.mr.state];
- },
- shouldRenderMergeHelp() {
- return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
- },
- shouldRenderPipelines() {
- return this.mr.hasCI;
- },
- shouldRenderRelatedLinks() {
- return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
- },
- shouldRenderSourceBranchRemovalStatus() {
- return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch &&
- (!this.mr.isNothingToMergeState && !this.mr.isMergedState);
- },
- },
- methods: {
- createService(store) {
- const endpoints = {
- mergePath: store.mergePath,
- mergeCheckPath: store.mergeCheckPath,
- cancelAutoMergePath: store.cancelAutoMergePath,
- removeWIPPath: store.removeWIPPath,
- sourceBranchPath: store.sourceBranchPath,
- ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
- statusPath: store.statusPath,
- mergeActionsContentPath: store.mergeActionsContentPath,
- rebasePath: store.rebasePath,
- };
- return new MRWidgetService(endpoints);
- },
- checkStatus(cb) {
- return this.service.checkStatus()
- .then(res => res.data)
- .then((data) => {
- this.handleNotification(data);
- this.mr.setData(data);
- this.setFaviconHelper();
-
- if (cb) {
- cb.call(null, data);
- }
- })
- .catch(() => new Flash('Something went wrong. Please try again.'));
- },
- initPolling() {
- this.pollingInterval = new SmartInterval({
- callback: this.checkStatus,
- startingInterval: 10000,
- maxInterval: 30000,
- hiddenInterval: 120000,
- incrementByFactorOf: 5000,
- });
- },
- initDeploymentsPolling() {
- this.deploymentsInterval = new SmartInterval({
- callback: this.fetchDeployments,
- startingInterval: 30000,
- maxInterval: 120000,
- hiddenInterval: 240000,
- incrementByFactorOf: 15000,
- immediateExecution: true,
- });
- },
- setFaviconHelper() {
- if (this.mr.ciStatusFaviconPath) {
- setFavicon(this.mr.ciStatusFaviconPath);
- }
- },
- fetchDeployments() {
- return this.service.fetchDeployments()
- .then(res => res.data)
- .then((data) => {
- if (data.length) {
- this.mr.deployments = data;
- }
- })
- .catch(() => {
- new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
- });
- },
- fetchActionsContent() {
- this.service.fetchMergeActionsContent()
- .then((res) => {
- if (res.data) {
- const el = document.createElement('div');
- el.innerHTML = res.data;
- document.body.appendChild(el);
- Project.initRefSwitcher();
- }
- })
- .catch(() => new Flash('Something went wrong. Please try again.'));
- },
- handleNotification(data) {
- if (data.ci_status === this.mr.ciStatus) return;
- if (!data.pipeline) return;
-
- const label = data.pipeline.details.status.label;
- const title = `Pipeline ${label}`;
- const message = `Pipeline ${label} for "${data.title}"`;
-
- notify.notifyMe(title, message, this.mr.gitlabLogo);
- },
- resumePolling() {
- this.pollingInterval.resume();
- },
- stopPolling() {
- this.pollingInterval.stopTimer();
- },
- bindEventHubListeners() {
- eventHub.$on('MRWidgetUpdateRequested', (cb) => {
- this.checkStatus(cb);
- });
-
- // `params` should be an Array contains a Boolean, like `[true]`
- // Passing parameter as Boolean didn't work.
- eventHub.$on('SetBranchRemoveFlag', (params) => {
- this.mr.isRemovingSourceBranch = params[0];
- });
-
- eventHub.$on('FailedToMerge', (mergeError) => {
- this.mr.state = 'failedToMerge';
- this.mr.mergeError = mergeError;
- });
-
- eventHub.$on('UpdateWidgetData', (data) => {
- this.mr.setData(data);
- });
-
- eventHub.$on('FetchActionsContent', () => {
- this.fetchActionsContent();
- });
-
- eventHub.$on('EnablePolling', () => {
- this.resumePolling();
- });
-
- eventHub.$on('DisablePolling', () => {
- this.stopPolling();
- });
- },
- handleMounted() {
- this.setFaviconHelper();
- this.initDeploymentsPolling();
- },
- },
- created() {
- this.initPolling();
- this.bindEventHubListeners();
- },
- mounted() {
- this.handleMounted();
- },
- components: {
- 'mr-widget-header': WidgetHeader,
- 'mr-widget-merge-help': WidgetMergeHelp,
- 'mr-widget-pipeline': WidgetPipeline,
- Deployment,
- 'mr-widget-maintainer-edit': WidgetMaintainerEdit,
- 'mr-widget-related-links': WidgetRelatedLinks,
- 'mr-widget-merged': MergedState,
- 'mr-widget-closed': ClosedState,
- 'mr-widget-merging': MergingState,
- 'mr-widget-failed-to-merge': FailedToMerge,
- 'mr-widget-wip': WipState,
- 'mr-widget-archived': ArchivedState,
- 'mr-widget-conflicts': ConflictsState,
- 'mr-widget-nothing-to-merge': NothingToMergeState,
- 'mr-widget-not-allowed': NotAllowedState,
- 'mr-widget-missing-branch': MissingBranchState,
- 'mr-widget-ready-to-merge': ReadyToMergeState,
- 'mr-widget-sha-mismatch': ShaMismatchState,
- 'mr-widget-squash-before-merge': SquashBeforeMerge,
- 'mr-widget-checking': CheckingState,
- 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
- 'mr-widget-pipeline-blocked': PipelineBlockedState,
- 'mr-widget-pipeline-failed': PipelineFailedState,
- 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
- 'mr-widget-auto-merge-failed': AutoMergeFailed,
- 'mr-widget-rebase': RebaseState,
- SourceBranchRemovalStatus,
- },
- template: `
- <div class="mr-state-widget prepend-top-default">
- <mr-widget-header :mr="mr" />
- <mr-widget-pipeline
- v-if="shouldRenderPipelines"
- :pipeline="mr.pipeline"
- :ci-status="mr.ciStatus"
- :has-ci="mr.hasCI"
- />
- <deployment
- v-for="deployment in mr.deployments"
- :key="deployment.id"
- :deployment="deployment"
- />
- <div class="mr-widget-section">
- <component
- :is="componentName"
- :mr="mr"
- :service="service" />
- <mr-widget-maintainer-edit
- :maintainerEditAllowed="mr.maintainerEditAllowed" />
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :state="mr.state"
- :related-links="mr.relatedLinks" />
- <source-branch-removal-status
- v-if="shouldRenderSourceBranchRemovalStatus"
- />
- </div>
- <div
- class="mr-widget-footer"
- v-if="shouldRenderMergeHelp">
- <mr-widget-merge-help />
- </div>
- </div>
- `,
-};
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
new file mode 100644
index 00000000000..f69fe03fcb3
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -0,0 +1,291 @@
+<script>
+
+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 { setFavicon } from '../lib/utils/common_utils';
+
+export default {
+ el: '#js-vue-mr-widget',
+ name: 'MRWidget',
+ components: {
+ 'mr-widget-header': WidgetHeader,
+ 'mr-widget-merge-help': WidgetMergeHelp,
+ 'mr-widget-pipeline': WidgetPipeline,
+ Deployment,
+ 'mr-widget-related-links': WidgetRelatedLinks,
+ 'mr-widget-merged': MergedState,
+ 'mr-widget-closed': ClosedState,
+ 'mr-widget-merging': MergingState,
+ 'mr-widget-failed-to-merge': FailedToMerge,
+ 'mr-widget-wip': WorkInProgressState,
+ 'mr-widget-archived': ArchivedState,
+ 'mr-widget-conflicts': ConflictsState,
+ 'mr-widget-nothing-to-merge': NothingToMergeState,
+ 'mr-widget-not-allowed': NotAllowedState,
+ 'mr-widget-missing-branch': MissingBranchState,
+ 'mr-widget-ready-to-merge': ReadyToMergeState,
+ 'sha-mismatch': ShaMismatchState,
+ 'mr-widget-squash-before-merge': SquashBeforeMerge,
+ 'mr-widget-checking': CheckingState,
+ 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
+ 'mr-widget-pipeline-blocked': PipelineBlockedState,
+ 'mr-widget-pipeline-failed': PipelineFailedState,
+ 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
+ 'mr-widget-auto-merge-failed': AutoMergeFailed,
+ 'mr-widget-rebase': RebaseState,
+ SourceBranchRemovalStatus,
+ },
+ props: {
+ mrData: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData);
+ const service = this.createService(store);
+ return {
+ mr: store,
+ service,
+ };
+ },
+ computed: {
+ componentName() {
+ return stateMaps.stateToComponentMap[this.mr.state];
+ },
+ shouldRenderMergeHelp() {
+ return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
+ },
+ shouldRenderPipelines() {
+ return this.mr.hasCI;
+ },
+ shouldRenderRelatedLinks() {
+ return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
+ },
+ shouldRenderSourceBranchRemovalStatus() {
+ return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch &&
+ (!this.mr.isNothingToMergeState && !this.mr.isMergedState);
+ },
+ },
+ created() {
+ this.initPolling();
+ this.bindEventHubListeners();
+ },
+ mounted() {
+ this.handleMounted();
+ },
+ methods: {
+ createService(store) {
+ const endpoints = {
+ mergePath: store.mergePath,
+ mergeCheckPath: store.mergeCheckPath,
+ cancelAutoMergePath: store.cancelAutoMergePath,
+ removeWIPPath: store.removeWIPPath,
+ sourceBranchPath: store.sourceBranchPath,
+ ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
+ statusPath: store.statusPath,
+ mergeActionsContentPath: store.mergeActionsContentPath,
+ rebasePath: store.rebasePath,
+ };
+ return new MRWidgetService(endpoints);
+ },
+ checkStatus(cb) {
+ return this.service.checkStatus()
+ .then(res => res.data)
+ .then((data) => {
+ this.handleNotification(data);
+ this.mr.setData(data);
+ this.setFaviconHelper();
+
+ if (cb) {
+ cb.call(null, data);
+ }
+ })
+ .catch(() => createFlash('Something went wrong. Please try again.'));
+ },
+ initPolling() {
+ this.pollingInterval = new SmartInterval({
+ callback: this.checkStatus,
+ startingInterval: 10000,
+ maxInterval: 30000,
+ hiddenInterval: 120000,
+ incrementByFactorOf: 5000,
+ });
+ },
+ initDeploymentsPolling() {
+ this.deploymentsInterval = new SmartInterval({
+ callback: this.fetchDeployments,
+ startingInterval: 30000,
+ maxInterval: 120000,
+ hiddenInterval: 240000,
+ incrementByFactorOf: 15000,
+ immediateExecution: true,
+ });
+ },
+ setFaviconHelper() {
+ if (this.mr.ciStatusFaviconPath) {
+ setFavicon(this.mr.ciStatusFaviconPath);
+ }
+ },
+ fetchDeployments() {
+ return this.service.fetchDeployments()
+ .then(res => res.data)
+ .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.'); // eslint-disable-line
+ });
+ },
+ fetchActionsContent() {
+ this.service.fetchMergeActionsContent()
+ .then((res) => {
+ if (res.data) {
+ const el = document.createElement('div');
+ el.innerHTML = res.data;
+ document.body.appendChild(el);
+ Project.initRefSwitcher();
+ }
+ })
+ .catch(() => createFlash('Something went wrong. Please try again.'));
+ },
+ handleNotification(data) {
+ if (data.ci_status === this.mr.ciStatus) return;
+ if (!data.pipeline) return;
+
+ const label = data.pipeline.details.status.label;
+ const title = `Pipeline ${label}`;
+ const message = `Pipeline ${label} for "${data.title}"`;
+
+ notify.notifyMe(title, message, this.mr.gitlabLogo);
+ },
+ resumePolling() {
+ this.pollingInterval.resume();
+ },
+ stopPolling() {
+ this.pollingInterval.stopTimer();
+ },
+ bindEventHubListeners() {
+ eventHub.$on('MRWidgetUpdateRequested', (cb) => {
+ this.checkStatus(cb);
+ });
+
+ // `params` should be an Array contains a Boolean, like `[true]`
+ // Passing parameter as Boolean didn't work.
+ eventHub.$on('SetBranchRemoveFlag', (params) => {
+ this.mr.isRemovingSourceBranch = params[0];
+ });
+
+ eventHub.$on('FailedToMerge', (mergeError) => {
+ this.mr.state = 'failedToMerge';
+ this.mr.mergeError = mergeError;
+ });
+
+ eventHub.$on('UpdateWidgetData', (data) => {
+ this.mr.setData(data);
+ });
+
+ eventHub.$on('FetchActionsContent', () => {
+ this.fetchActionsContent();
+ });
+
+ eventHub.$on('EnablePolling', () => {
+ this.resumePolling();
+ });
+
+ eventHub.$on('DisablePolling', () => {
+ this.stopPolling();
+ });
+ },
+ handleMounted() {
+ this.setFaviconHelper();
+ this.initDeploymentsPolling();
+ },
+ },
+};
+</script>
+<template>
+ <div class="mr-state-widget prepend-top-default">
+ <mr-widget-header
+ :mr="mr"
+ />
+ <mr-widget-pipeline
+ v-if="shouldRenderPipelines"
+ :pipeline="mr.pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ />
+ <deployment
+ v-for="deployment in mr.deployments"
+ :key="deployment.id"
+ :deployment="deployment"
+ />
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service"
+ />
+
+ <section
+ v-if="mr.maintainerEditAllowed"
+ class="mr-info-list mr-links"
+ >
+ {{ s__("mrWidget|Allows edits from maintainers") }}
+ </section>
+
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ />
+
+ <source-branch-removal-status
+ v-if="shouldRenderSourceBranchRemovalStatus"
+ />
+ </div>
+ <div
+ class="mr-widget-footer"
+ v-if="shouldRenderMergeHelp"
+ >
+ <mr-widget-merge-help />
+ </div>
+ </div>
+</template>
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 a47ca9fae86..83b7b054e6f 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
@@ -20,6 +20,7 @@ export default class MergeRequestStore {
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
+ this.shortMergeCommitSha = data.short_merge_commit_sha;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
@@ -65,6 +66,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
+ this.mergeCommitPath = data.merge_commit_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
diff --git a/app/assets/javascripts/vue_shared/components/callout.vue b/app/assets/javascripts/vue_shared/components/callout.vue
new file mode 100644
index 00000000000..ccf802c456c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/callout.vue
@@ -0,0 +1,27 @@
+<script>
+const calloutVariants = ['danger', 'success', 'info', 'warning'];
+
+export default {
+ props: {
+ category: {
+ type: String,
+ required: false,
+ default: calloutVariants[0],
+ validator: value => calloutVariants.includes(value),
+ },
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :class="`bs-callout bs-callout-${category}`"
+ role="alert"
+ aria-live="assertive"
+ >
+ {{ message }}
+ </div>
+</template>
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 5324d5dc797..0d64efcbf68 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,52 +1,52 @@
<script>
- 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.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- */
+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.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table - first column
+ * - Jobs table - first column
+ * - Pipeline show view - header
+ * - Job show view - header
+ * - MR widget
+ */
- export default {
- components: {
- ciIcon,
+export default {
+ components: {
+ CiIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ showText: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- showText: {
- type: Boolean,
- required: false,
- default: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const className = this.status.group;
+ return className ? `ci-status ci-${className}` : 'ci-status';
},
- computed: {
- cssClass() {
- const className = this.status.group;
- return className ? `ci-status ci-${className}` : 'ci-status';
- },
- },
- };
+ },
+};
</script>
<template>
<a
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 8fea746f4de..fcab8f571dd 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,45 +1,44 @@
<script>
- import icon from '../../vue_shared/components/icon.vue';
+import Icon from '../../vue_shared/components/icon.vue';
- /**
- * Renders CI icon based on API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table Badge
- * - Pipelines table mini graph
- * - Pipeline graph
- * - Pipeline show view badge
- * - Jobs table
- * - Jobs show view header
- * - Jobs show view sidebar
- */
- export default {
- components: {
- icon,
+/**
+ * Renders CI icon based on API response shared between all places where it is used.
+ *
+ * Receives status object containing:
+ * status: {
+ * details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
+ * group:"running" // used for CSS class
+ * icon: "icon_status_running" // used to render the icon
+ * label:"running" // used for potential tooltip
+ * text:"running" // text rendered
+ * }
+ *
+ * Used in:
+ * - Pipelines table Badge
+ * - Pipelines table mini graph
+ * - Pipeline graph
+ * - Pipeline show view badge
+ * - Jobs table
+ * - Jobs show view header
+ * - Jobs show view sidebar
+ */
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ cssClass() {
+ const status = this.status.group;
+ return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
-
- computed: {
- cssClass() {
- const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
- },
- },
- };
+ },
+};
</script>
<template>
<span :class="cssClass">
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index cab126a7eca..cb2cc3901ad 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -1,40 +1,50 @@
<script>
- /**
- * Falls back to the code used in `copy_to_clipboard.js`
- */
- import tooltip from '../directives/tooltip';
+/**
+ * Falls back to the code used in `copy_to_clipboard.js`
+ *
+ * Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
+ * when clicked.
+ *
+ * @example
+ * <clipboard-button
+ * title="Copy to clipbard"
+ * text="Content to be copied"
+ * css-class="btn-transparent"
+ * />
+ */
+import tooltip from '../directives/tooltip';
- export default {
- name: 'ClipboardButton',
- directives: {
- tooltip,
+export default {
+ name: 'ClipboardButton',
+ directives: {
+ tooltip,
+ },
+ props: {
+ text: {
+ type: String,
+ required: true,
},
- props: {
- text: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- tooltipContainer: {
- type: [String, Boolean],
- required: false,
- default: false,
- },
- cssClass: {
- type: String,
- required: false,
- default: 'btn-default',
- },
+ title: {
+ type: String,
+ required: true,
},
- };
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ tooltipContainer: {
+ type: [String, Boolean],
+ required: false,
+ default: false,
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index b8875d04488..8f250a6c989 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,119 +1,111 @@
<script>
- import commitIconSvg from 'icons/_icon_commit.svg';
- import userAvatarLink from './user_avatar/user_avatar_link.vue';
- import tooltip from '../directives/tooltip';
- import icon from '../../vue_shared/components/icon.vue';
+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,
+export default {
+ directives: {
+ tooltip,
+ },
+ components: {
+ UserAvatarLink,
+ Icon,
+ },
+ props: {
+ /**
+ * Indicates the existance 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
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- components: {
- userAvatarLink,
- icon,
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- /**
- * Indicates the existance 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
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- showBranch: {
- type: Boolean,
- required: false,
- default: true,
- },
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ showBranch: {
+ type: Boolean,
+ required: false,
+ default: true,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.path &&
- this.author.username;
- },
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
+ },
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
- created() {
- this.commitIconSvg = commitIconSvg;
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author && this.author.avatar_url && this.author.path && this.author.username;
},
- };
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+};
</script>
<template>
<div class="branch-commit">
@@ -141,11 +133,10 @@
{{ commitRef.name }}
</a>
</template>
- <div
- v-html="commitIconSvg"
+ <icon
+ name="commit"
class="commit-icon js-commit-icon"
- >
- </div>
+ />
<a
class="commit-sha"
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 395a71acccf..7b5367ac19b 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
@@ -42,7 +42,7 @@ export default {
target="_blank">
<icon
name="download"
- css-classes="pull-left append-right-8"
+ css-classes="float-left append-right-8"
:size="16"
/>
{{ __('Download') }}
diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
index dcf1489b37c..424af5a0293 100644
--- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
@@ -87,7 +87,7 @@
<div
:id="id"
class="modal"
- :class="id ? '' : 'show'"
+ :class="id ? '' : 'd-block'"
role="dialog"
tabindex="-1"
>
@@ -99,12 +99,12 @@
<div class="modal-content">
<div class="modal-header">
<slot name="header">
- <h4 class="modal-title pull-left">
+ <h4 class="modal-title float-left">
{{ title }}
</h4>
<button
type="button"
- class="close pull-right"
+ class="close float-right"
@click="emitCancel($event)"
data-dismiss="modal"
aria-label="Close"
@@ -166,7 +166,7 @@
</div>
<div
v-if="!id"
- class="modal-backdrop fade in"
+ class="modal-backdrop fade show"
>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index c943c8d98a4..9295be3e2b2 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,33 +1,33 @@
<script>
- import { __ } from '~/locale';
- /**
- * Port of detail_behavior expand button.
- *
- * @example
- * <expand-button>
- * <template slot="expanded">
- * Text goes here.
- * </template>
- * </expand-button>
- */
- export default {
- name: 'ExpandButton',
- data() {
- return {
- isCollapsed: true,
- };
+import { __ } from '~/locale';
+/**
+ * Port of detail_behavior expand button.
+ *
+ * @example
+ * <expand-button>
+ * <template slot="expanded">
+ * Text goes here.
+ * </template>
+ * </expand-button>
+ */
+export default {
+ name: 'ExpandButton',
+ data() {
+ return {
+ isCollapsed: true,
+ };
+ },
+ computed: {
+ ariaLabel() {
+ return __('Click to expand text');
},
- computed: {
- ariaLabel() {
- return __('Click to expand text');
- },
+ },
+ methods: {
+ onClick() {
+ this.isCollapsed = !this.isCollapsed;
},
- methods: {
- onClick() {
- this.isCollapsed = !this.isCollapsed;
- },
- },
- };
+ },
+};
</script>
<template>
<span>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index ee1c3498748..be2755452e2 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -1,9 +1,9 @@
<script>
- import getIconForFile from './file_icon/file_icon_map';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
+import getIconForFile from './file_icon/file_icon_map';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import icon from '../../vue_shared/components/icon.vue';
- /* This is a re-usable vue component for rendering a svg sprite
+/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
@@ -15,60 +15,60 @@
/>
*/
- export default {
- components: {
- loadingIcon,
- icon,
+export default {
+ components: {
+ loadingIcon,
+ icon,
+ },
+ props: {
+ fileName: {
+ type: String,
+ required: true,
},
- props: {
- fileName: {
- type: String,
- required: true,
- },
- folder: {
- type: Boolean,
- required: false,
- default: false,
- },
+ folder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- opened: {
- type: Boolean,
- required: false,
- default: false,
- },
+ opened: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ spriteHref() {
+ const iconName = getIconForFile(this.fileName) || 'file';
+ return `${gon.sprite_file_icons}#${iconName}`;
+ },
+ folderIconName() {
+ return this.opened ? 'folder-open' : 'folder';
},
- computed: {
- spriteHref() {
- const iconName = getIconForFile(this.fileName) || 'file';
- return `${gon.sprite_file_icons}#${iconName}`;
- },
- folderIconName() {
- return this.opened ? 'folder-open' : 'folder';
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
<span>
@@ -82,6 +82,7 @@
v-if="!loading && folder"
:name="folderIconName"
:size="size"
+ css-classes="folder-icon"
/>
<loading-icon
v-if="loading"
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
index f28e5e2715d..d5d5a7d3798 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -53,6 +53,11 @@ export default {
<div class="modal-content">
<div class="modal-header">
<slot name="header">
+ <h4 class="modal-title">
+ <slot name="title">
+ {{ headerTitleText }}
+ </slot>
+ </h4>
<button
type="button"
class="close js-modal-close-action"
@@ -62,11 +67,6 @@ export default {
>
<span aria-hidden="true">&times;</span>
</button>
- <h4 class="modal-title">
- <slot name="title">
- {{ headerTitleText }}
- </slot>
- </h4>
</slot>
</div>
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 a0cd0cbd200..ca17fa06a00 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,78 +1,78 @@
<script>
- import ciIconBadge from './ci_badge_link.vue';
- import loadingIcon from './loading_icon.vue';
- import timeagoTooltip from './time_ago_tooltip.vue';
- import tooltip from '../directives/tooltip';
- import userAvatarImage from './user_avatar/user_avatar_image.vue';
+import CiIconBadge from './ci_badge_link.vue';
+import LoadingIcon from './loading_icon.vue';
+import TimeagoTooltip from './time_ago_tooltip.vue';
+import tooltip from '../directives/tooltip';
+import UserAvatarImage from './user_avatar/user_avatar_image.vue';
- /**
- * Renders header component for job and pipeline page based on UI mockups
- *
- * Used in:
- * - job show page
- * - pipeline show page
- */
- export default {
- components: {
- ciIconBadge,
- loadingIcon,
- timeagoTooltip,
- userAvatarImage,
+/**
+ * Renders header component for job and pipeline page based on UI mockups
+ *
+ * Used in:
+ * - job show page
+ * - pipeline show page
+ */
+export default {
+ components: {
+ CiIconBadge,
+ LoadingIcon,
+ TimeagoTooltip,
+ UserAvatarImage,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ status: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
+ itemName: {
+ type: String,
+ required: true,
},
- props: {
- status: {
- type: Object,
- required: true,
- },
- itemName: {
- type: String,
- required: true,
- },
- itemId: {
- type: Number,
- required: true,
- },
- time: {
- type: String,
- required: true,
- },
- user: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
- hasSidebarButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldRenderTriggeredLabel: {
- type: Boolean,
- required: false,
- default: true,
- },
+ itemId: {
+ type: Number,
+ required: true,
},
+ time: {
+ type: String,
+ required: true,
+ },
+ user: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ hasSidebarButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ shouldRenderTriggeredLabel: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
- computed: {
- userAvatarAltText() {
- return `${this.user.name}'s avatar`;
- },
+ computed: {
+ userAvatarAltText() {
+ return `${this.user.name}'s avatar`;
},
+ },
- methods: {
- onClickAction(action) {
- this.$emit('actionClicked', action);
- },
+ methods: {
+ onClickAction(action) {
+ this.$emit('actionClicked', action);
},
- };
+ },
+};
</script>
<template>
@@ -163,8 +163,8 @@
<button
v-if="hasSidebarButton"
type="button"
- class="btn btn-default visible-xs-block
-visible-sm-block sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
+ class="btn btn-default d-block d-sm-none d-md-none
+sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header"
aria-label="Toggle Sidebar"
id="toggleSidebar"
>
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 6a2e05000e1..c42c4a1fbe7 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -1,85 +1,88 @@
<script>
+/* This is a re-usable vue component for rendering a svg sprite
+ icon
- /* This is a re-usable vue component for rendering a svg sprite
- icon
+ Sample configuration:
- Sample configuration:
+ <icon
+ name="retry"
+ :size="32"
+ css-classes="top"
+ />
- <icon
- name="retry"
- :size="32"
- css-classes="top"
- />
+*/
+// only allow classes in images.scss e.g. s12
+const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
- */
- // only allow classes in images.scss e.g. s12
- const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
-
- export default {
- props: {
- name: {
- type: String,
- required: true,
- },
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
- size: {
- type: Number,
- required: false,
- default: 16,
- validator(value) {
- return validSizes.includes(value);
- },
+ size: {
+ type: Number,
+ required: false,
+ default: 16,
+ validator(value) {
+ return validSizes.includes(value);
},
+ },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
- width: {
- type: Number,
- required: false,
- default: null,
- },
+ width: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- height: {
- type: Number,
- required: false,
- default: null,
- },
+ height: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- y: {
- type: Number,
- required: false,
- default: null,
- },
+ y: {
+ type: Number,
+ required: false,
+ default: null,
+ },
- x: {
- type: Number,
- required: false,
- default: null,
- },
+ x: {
+ type: Number,
+ required: false,
+ default: null,
},
+ },
- computed: {
- spriteHref() {
- return `${gon.sprite_icons}#${this.name}`;
- },
- iconSizeClass() {
- return this.size ? `s${this.size}` : '';
- },
+ computed: {
+ spriteHref() {
+ return `${gon.sprite_icons}#${this.name}`;
+ },
+ iconTestClass() {
+ return `ic-${this.name}`;
+ },
+ iconSizeClass() {
+ return this.size ? `s${this.size}` : '';
},
- };
+ },
+};
</script>
<template>
<svg
- :class="[iconSizeClass, cssClasses]"
+ :class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width"
:height="height"
:x="x"
- :y="y">
+ :y="y"
+ >
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
index 0a30f467b08..23010f40f26 100644
--- a/app/assets/javascripts/vue_shared/components/identicon.vue
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -17,7 +17,7 @@ export default {
},
computed: {
/**
- * This method is based on app/helpers/application_helper.rb#project_identicon
+ * This method is based on app/helpers/avatars_helper.rb#project_identicon
*/
identiconStyles() {
const allowedColors = [
diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue
index e832d94d32f..88c13a1f340 100644
--- a/app/assets/javascripts/vue_shared/components/loading_button.vue
+++ b/app/assets/javascripts/vue_shared/components/loading_button.vue
@@ -70,12 +70,14 @@
/>
</transition>
<transition name="fade">
- <span
- v-if="label"
- class="js-loading-button-label"
- >
- {{ label }}
- </span>
+ <slot>
+ <span
+ v-if="label"
+ class="js-loading-button-label"
+ >
+ {{ label }}
+ </span>
+ </slot>
</transition>
</button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index c0ee88bbf72..d63318f3da6 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -111,7 +111,7 @@
Attach a file
</button>
<button
- class="btn btn-default btn-xs hide button-cancel-uploading-files"
+ class="btn btn-default btn-sm hide button-cancel-uploading-files"
type="button"
>
Cancel
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index b33a0101dbf..08d4936f480 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,53 +1,53 @@
<script>
- import $ from 'jquery';
+import $ from 'jquery';
- /**
- * Given an array of tabs, renders non linked bootstrap tabs.
- * When a tab is clicked it will trigger an event and provide the clicked scope.
- *
- * This component is used in apps that handle the API call.
- * If you only need to change the URL this component should not be used.
- *
- * @example
- * <navigation-tabs
- * :tabs="[
- * {
- * name: String,
- * scope: String,
- * count: Number || Undefined,
- * isActive: Boolean,
- * },
- * ]"
- * @onChangeTab="onChangeTab"
- * />
- */
- export default {
- name: 'NavigationTabs',
- props: {
- tabs: {
- type: Array,
- required: true,
- },
- scope: {
- type: String,
- required: false,
- default: '',
- },
+/**
+ * Given an array of tabs, renders non linked bootstrap tabs.
+ * When a tab is clicked it will trigger an event and provide the clicked scope.
+ *
+ * This component is used in apps that handle the API call.
+ * If you only need to change the URL this component should not be used.
+ *
+ * @example
+ * <navigation-tabs
+ * :tabs="[
+ * {
+ * name: String,
+ * scope: String,
+ * count: Number || Undefined || Null,
+ * isActive: Boolean,
+ * },
+ * ]"
+ * @onChangeTab="onChangeTab"
+ * />
+ */
+export default {
+ name: 'NavigationTabs',
+ props: {
+ tabs: {
+ type: Array,
+ required: true,
},
- mounted() {
- $(document).trigger('init.scrolling-tabs');
+ scope: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+ methods: {
+ shouldRenderBadge(count) {
+ // 0 is valid in a badge, but evaluates to false, we need to check for undefined or null
+ return !(count === undefined || count === null);
},
- methods: {
- shouldRenderBadge(count) {
- // 0 is valid in a badge, but evaluates to false, we need to check for undefined
- return count !== undefined;
- },
- onTabClick(tab) {
- this.$emit('onChangeTab', tab.scope);
- },
+ onTabClick(tab) {
+ this.$emit('onChangeTab', tab.scope);
},
- };
+ },
+};
</script>
<template>
<ul class="nav-links scrolling-tabs separator">
@@ -67,7 +67,7 @@
<span
v-if="shouldRenderBadge(tab.count)"
- class="badge"
+ class="badge badge-pill"
>
{{ tab.count }}
</span>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 50b1508691b..eccba61a8c0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -54,7 +54,7 @@
<div class="note-header">
<div class="note-header-info">
<a :href="getUserData.path">
- <span class="hidden-xs">{{ getUserData.name }}</span>
+ <span class="d-none d-sm-block">{{ getUserData.name }}</span>
<span class="note-headline-light">@{{ getUserData.username }}</span>
</a>
</div>
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 3fcacd156c5..71ec34f2c7a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -116,7 +116,7 @@
v-if="isLoading"
:inline="true"
/>
- <div class="pull-right">
+ <div class="float-right">
<button
v-if="editable && !editing"
type="button"
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 5ede53d8d01..70b46a9c2bb 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
@@ -1,4 +1,5 @@
<script>
+import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
+ $(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
+ handleCollapsedValueClick() {
+ this.$emit('toggleCollapse');
+ },
+ handleDropdownHidden() {
+ this.$emit('onDropdownClose');
+ },
},
};
</script>
@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
+ @onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
:can-edit="canEdit"
@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName"
:label="label"
/>
- <div class="dropdown">
+ <div
+ class="dropdown"
+ ref="dropdown"
+ >
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
index 34a07f33a23..3c400afdc1d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -77,13 +77,13 @@ export default {
<div class="clearfix">
<button
type="button"
- class="btn btn-primary pull-left js-new-label-btn disabled"
+ class="btn btn-primary float-left js-new-label-btn disabled"
>
{{ __('Create') }}
</button>
<button
type="button"
- class="btn btn-default pull-right js-cancel-label-btn"
+ class="btn btn-default float-right js-cancel-label-btn"
>
{{ __('Cancel') }}
</button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
index 7da82e90e29..9ac32ff13c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -21,7 +21,7 @@ export default {
</i>
<button
type="button"
- class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
+ class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle"
>
{{ __('Edit') }}
</button>
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 5cf728fe050..68fa2ab8d01 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
@@ -26,6 +26,11 @@ export default {
return labelsString;
},
},
+ methods: {
+ handleClick() {
+ this.$emit('onValueClick');
+ },
+ },
};
</script>
@@ -36,6 +41,7 @@ export default {
data-placement="left"
data-container="body"
:title="labelsList"
+ @click="handleClick"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
index 8211d425b1f..de6f8c32e74 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -1,18 +1,29 @@
<script>
- export default {
- name: 'ToggleSidebar',
- props: {
- collapsed: {
- type: Boolean,
- required: true,
- },
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ name: 'ToggleSidebar',
+ directives: {
+ tooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tooltipLabel() {
+ return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
- methods: {
- toggle() {
- this.$emit('toggle');
- },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
},
- };
+ },
+};
</script>
<template>
@@ -20,6 +31,10 @@
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
+ v-tooltip
+ data-container="body"
+ data-placement="left"
+ :title="tooltipLabel"
>
<i
aria-label="toggle collapse"
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index bec4e7c99b6..368eeb6c453 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -40,7 +40,7 @@ export default {
:class="cssClass"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
- data-container="body">
- {{ timeFormated(time) }}
+ data-container="body"
+ v-text="timeFormated(time)">
</time>
</template>
diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js
index eb35294906b..c913bc34c68 100644
--- a/app/assets/javascripts/vue_shared/directives/popover.js
+++ b/app/assets/javascripts/vue_shared/directives/popover.js
@@ -17,6 +17,6 @@ export default {
},
unbind(el) {
- $(el).popover('destroy');
+ $(el).popover('dispose');
},
};
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index b7f7e9fec15..155e6b6698a 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -6,10 +6,10 @@ export default {
},
componentUpdated(el) {
- $(el).tooltip('fixTitle');
+ $(el).tooltip('_fixTitle');
},
unbind(el) {
- $(el).tooltip('destroy');
+ $(el).tooltip('dispose');
},
};
diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
index 70b9efe0c68..d29c7fe973a 100644
--- a/app/assets/javascripts/vue_shared/models/label.js
+++ b/app/assets/javascripts/vue_shared/models/label.js
@@ -1,4 +1,4 @@
-class ListLabel {
+export default class ListLabel {
constructor(obj) {
this.id = obj.id;
this.title = obj.title;
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
new file mode 100644
index 00000000000..3b7ee5c73e6
--- /dev/null
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -0,0 +1,149 @@
+/*
+ * Scss to help with bootstrap 3 to 4 migration
+ */
+
+$text-color: $gl-text-color;
+
+$brand-primary: $gl-primary;
+$brand-success: $gl-success;
+$brand-info: $gl-info;
+$brand-warning: $gl-warning;
+$brand-danger: $gl-danger;
+
+$border-radius-base: 3px !default;
+
+$modal-body-bg: $white-light;
+$input-border: $border-color;
+$input-border-focus: $focus-border-color;
+
+$padding-base-vertical: $gl-vert-padding;
+$padding-base-horizontal: $gl-padding;
+
+html {
+ // Override default font size used in bs4
+ font-size: 14px;
+}
+
+button,
+html [type="button"],
+[type="reset"],
+[type="submit"] {
+ // Override bootstrap reboot
+ -webkit-appearance: inherit;
+}
+
+[role="button"] {
+ cursor: pointer;
+}
+
+a {
+ color: $gl-link-color;
+}
+
+.form-group.row .col-form-label {
+ // Bootstrap 4 aligns labels to the left
+ // for horizontal forms
+ @include media-breakpoint-up(md) {
+ text-align: right;
+ }
+}
+
+table {
+ // Remove any table border lines
+ border-spacing: 0;
+}
+
+.tooltip {
+ // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders
+ pointer-events: none;
+}
+
+.popover {
+ font-size: 14px;
+}
+
+@each $breakpoint in map-keys($grid-breakpoints) {
+ @include media-breakpoint-up($breakpoint) {
+ $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+
+ .d#{$infix}-table-header-group { display: table-header-group !important; }
+ }
+}
+
+.text-secondary {
+ // Override Bootstrap's light secondary color
+ // We have to use !important because bootstrap has that set as well
+ color: $gl-text-color-secondary !important;
+}
+
+// Polyfill deprecated selectors
+
+.hidden {
+ display: none !important;
+ visibility: hidden !important;
+}
+
+.hide {
+ display: none;
+}
+
+.dropdown-toggle::after {
+ // Remove bootstrap's dropdown caret
+ display: none;
+}
+
+.badge {
+ padding: 4px 5px;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: $gl-font-weight-normal;
+ display: inline-block;
+
+ &.badge-gray {
+ background-color: $label-gray-bg;
+ color: $gl-text-color;
+ text-shadow: none;
+ }
+
+ &.badge-inverse {
+ background-color: $label-inverse-bg;
+ }
+}
+
+.divider {
+ @extend .dropdown-divider;
+}
+
+.info-well {
+ background: $theme-gray-50;
+ color: $gl-text-color;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+ margin-bottom: 16px;
+
+ .well-segment {
+ padding: 16px;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid $well-inner-border;
+ }
+ }
+}
+
+.card {
+ &.card-without-border {
+ @extend .border-0;
+ }
+
+ &.card-without-margin {
+ margin: 0;
+ }
+
+ &.bg-light {
+ @extend .border-0;
+ }
+}
+
+.nav-tabs .nav-link {
+ border: 0;
+}
diff --git a/app/assets/stylesheets/emoji_sprites.scss b/app/assets/stylesheets/emoji_sprites.scss
new file mode 100644
index 00000000000..8f6134c474b
--- /dev/null
+++ b/app/assets/stylesheets/emoji_sprites.scss
@@ -0,0 +1,5403 @@
+// Automatic Prettier Formatting for this big file
+// scss-lint:disable EmptyLineBetweenBlocks
+.emoji-zzz {
+ background-position: 0 0;
+}
+.emoji-1234 {
+ background-position: -20px 0;
+}
+.emoji-1F627 {
+ background-position: 0 -20px;
+}
+.emoji-8ball {
+ background-position: -20px -20px;
+}
+.emoji-a {
+ background-position: -40px 0;
+}
+.emoji-ab {
+ background-position: -40px -20px;
+}
+.emoji-abc {
+ background-position: 0 -40px;
+}
+.emoji-abcd {
+ background-position: -20px -40px;
+}
+.emoji-accept {
+ background-position: -40px -40px;
+}
+.emoji-aerial_tramway {
+ background-position: -60px 0;
+}
+.emoji-airplane {
+ background-position: -60px -20px;
+}
+.emoji-airplane_arriving {
+ background-position: -60px -40px;
+}
+.emoji-airplane_departure {
+ background-position: 0 -60px;
+}
+.emoji-airplane_small {
+ background-position: -20px -60px;
+}
+.emoji-alarm_clock {
+ background-position: -40px -60px;
+}
+.emoji-alembic {
+ background-position: -60px -60px;
+}
+.emoji-alien {
+ background-position: -80px 0;
+}
+.emoji-ambulance {
+ background-position: -80px -20px;
+}
+.emoji-amphora {
+ background-position: -80px -40px;
+}
+.emoji-anchor {
+ background-position: -80px -60px;
+}
+.emoji-angel {
+ background-position: 0 -80px;
+}
+.emoji-angel_tone1 {
+ background-position: -20px -80px;
+}
+.emoji-angel_tone2 {
+ background-position: -40px -80px;
+}
+.emoji-angel_tone3 {
+ background-position: -60px -80px;
+}
+.emoji-angel_tone4 {
+ background-position: -80px -80px;
+}
+.emoji-angel_tone5 {
+ background-position: -100px 0;
+}
+.emoji-anger {
+ background-position: -100px -20px;
+}
+.emoji-anger_right {
+ background-position: -100px -40px;
+}
+.emoji-angry {
+ background-position: -100px -60px;
+}
+.emoji-ant {
+ background-position: -100px -80px;
+}
+.emoji-apple {
+ background-position: 0 -100px;
+}
+.emoji-aquarius {
+ background-position: -20px -100px;
+}
+.emoji-aries {
+ background-position: -40px -100px;
+}
+.emoji-arrow_backward {
+ background-position: -60px -100px;
+}
+.emoji-arrow_double_down {
+ background-position: -80px -100px;
+}
+.emoji-arrow_double_up {
+ background-position: -100px -100px;
+}
+.emoji-arrow_down {
+ background-position: -120px 0;
+}
+.emoji-arrow_down_small {
+ background-position: -120px -20px;
+}
+.emoji-arrow_forward {
+ background-position: -120px -40px;
+}
+.emoji-arrow_heading_down {
+ background-position: -120px -60px;
+}
+.emoji-arrow_heading_up {
+ background-position: -120px -80px;
+}
+.emoji-arrow_left {
+ background-position: -120px -100px;
+}
+.emoji-arrow_lower_left {
+ background-position: 0 -120px;
+}
+.emoji-arrow_lower_right {
+ background-position: -20px -120px;
+}
+.emoji-arrow_right {
+ background-position: -40px -120px;
+}
+.emoji-arrow_right_hook {
+ background-position: -60px -120px;
+}
+.emoji-arrow_up {
+ background-position: -80px -120px;
+}
+.emoji-arrow_up_down {
+ background-position: -100px -120px;
+}
+.emoji-arrow_up_small {
+ background-position: -120px -120px;
+}
+.emoji-arrow_upper_left {
+ background-position: -140px 0;
+}
+.emoji-arrow_upper_right {
+ background-position: -140px -20px;
+}
+.emoji-arrows_clockwise {
+ background-position: -140px -40px;
+}
+.emoji-arrows_counterclockwise {
+ background-position: -140px -60px;
+}
+.emoji-art {
+ background-position: -140px -80px;
+}
+.emoji-articulated_lorry {
+ background-position: -140px -100px;
+}
+.emoji-asterisk {
+ background-position: -140px -120px;
+}
+.emoji-astonished {
+ background-position: 0 -140px;
+}
+.emoji-athletic_shoe {
+ background-position: -20px -140px;
+}
+.emoji-atm {
+ background-position: -40px -140px;
+}
+.emoji-atom {
+ background-position: -60px -140px;
+}
+.emoji-avocado {
+ background-position: -80px -140px;
+}
+.emoji-b {
+ background-position: -100px -140px;
+}
+.emoji-baby {
+ background-position: -120px -140px;
+}
+.emoji-baby_bottle {
+ background-position: -140px -140px;
+}
+.emoji-baby_chick {
+ background-position: -160px 0;
+}
+.emoji-baby_symbol {
+ background-position: -160px -20px;
+}
+.emoji-baby_tone1 {
+ background-position: -160px -40px;
+}
+.emoji-baby_tone2 {
+ background-position: -160px -60px;
+}
+.emoji-baby_tone3 {
+ background-position: -160px -80px;
+}
+.emoji-baby_tone4 {
+ background-position: -160px -100px;
+}
+.emoji-baby_tone5 {
+ background-position: -160px -120px;
+}
+.emoji-back {
+ background-position: -160px -140px;
+}
+.emoji-bacon {
+ background-position: 0 -160px;
+}
+.emoji-badminton {
+ background-position: -20px -160px;
+}
+.emoji-baggage_claim {
+ background-position: -40px -160px;
+}
+.emoji-balloon {
+ background-position: -60px -160px;
+}
+.emoji-ballot_box {
+ background-position: -80px -160px;
+}
+.emoji-ballot_box_with_check {
+ background-position: -100px -160px;
+}
+.emoji-bamboo {
+ background-position: -120px -160px;
+}
+.emoji-banana {
+ background-position: -140px -160px;
+}
+.emoji-bangbang {
+ background-position: -160px -160px;
+}
+.emoji-bank {
+ background-position: -180px 0;
+}
+.emoji-bar_chart {
+ background-position: -180px -20px;
+}
+.emoji-barber {
+ background-position: -180px -40px;
+}
+.emoji-baseball {
+ background-position: -180px -60px;
+}
+.emoji-basketball {
+ background-position: -180px -80px;
+}
+.emoji-basketball_player {
+ background-position: -180px -100px;
+}
+.emoji-basketball_player_tone1 {
+ background-position: -180px -120px;
+}
+.emoji-basketball_player_tone2 {
+ background-position: -180px -140px;
+}
+.emoji-basketball_player_tone3 {
+ background-position: -180px -160px;
+}
+.emoji-basketball_player_tone4 {
+ background-position: 0 -180px;
+}
+.emoji-basketball_player_tone5 {
+ background-position: -20px -180px;
+}
+.emoji-bat {
+ background-position: -40px -180px;
+}
+.emoji-bath {
+ background-position: -60px -180px;
+}
+.emoji-bath_tone1 {
+ background-position: -80px -180px;
+}
+.emoji-bath_tone2 {
+ background-position: -100px -180px;
+}
+.emoji-bath_tone3 {
+ background-position: -120px -180px;
+}
+.emoji-bath_tone4 {
+ background-position: -140px -180px;
+}
+.emoji-bath_tone5 {
+ background-position: -160px -180px;
+}
+.emoji-bathtub {
+ background-position: -180px -180px;
+}
+.emoji-battery {
+ background-position: -200px 0;
+}
+.emoji-beach {
+ background-position: -200px -20px;
+}
+.emoji-beach_umbrella {
+ background-position: -200px -40px;
+}
+.emoji-bear {
+ background-position: -200px -60px;
+}
+.emoji-bed {
+ background-position: -200px -80px;
+}
+.emoji-bee {
+ background-position: -200px -100px;
+}
+.emoji-beer {
+ background-position: -200px -120px;
+}
+.emoji-beers {
+ background-position: -200px -140px;
+}
+.emoji-beetle {
+ background-position: -200px -160px;
+}
+.emoji-beginner {
+ background-position: -200px -180px;
+}
+.emoji-bell {
+ background-position: 0 -200px;
+}
+.emoji-bellhop {
+ background-position: -20px -200px;
+}
+.emoji-bento {
+ background-position: -40px -200px;
+}
+.emoji-bicyclist {
+ background-position: -60px -200px;
+}
+.emoji-bicyclist_tone1 {
+ background-position: -80px -200px;
+}
+.emoji-bicyclist_tone2 {
+ background-position: -100px -200px;
+}
+.emoji-bicyclist_tone3 {
+ background-position: -120px -200px;
+}
+.emoji-bicyclist_tone4 {
+ background-position: -140px -200px;
+}
+.emoji-bicyclist_tone5 {
+ background-position: -160px -200px;
+}
+.emoji-bike {
+ background-position: -180px -200px;
+}
+.emoji-bikini {
+ background-position: -200px -200px;
+}
+.emoji-biohazard {
+ background-position: -220px 0;
+}
+.emoji-bird {
+ background-position: -220px -20px;
+}
+.emoji-birthday {
+ background-position: -220px -40px;
+}
+.emoji-black_circle {
+ background-position: -220px -60px;
+}
+.emoji-black_heart {
+ background-position: -220px -80px;
+}
+.emoji-black_joker {
+ background-position: -220px -100px;
+}
+.emoji-black_large_square {
+ background-position: -220px -120px;
+}
+.emoji-black_medium_small_square {
+ background-position: -220px -140px;
+}
+.emoji-black_medium_square {
+ background-position: -220px -160px;
+}
+.emoji-black_nib {
+ background-position: -220px -180px;
+}
+.emoji-black_small_square {
+ background-position: -220px -200px;
+}
+.emoji-black_square_button {
+ background-position: 0 -220px;
+}
+.emoji-blossom {
+ background-position: -20px -220px;
+}
+.emoji-blowfish {
+ background-position: -40px -220px;
+}
+.emoji-blue_book {
+ background-position: -60px -220px;
+}
+.emoji-blue_car {
+ background-position: -80px -220px;
+}
+.emoji-blue_heart {
+ background-position: -100px -220px;
+}
+.emoji-blush {
+ background-position: -120px -220px;
+}
+.emoji-boar {
+ background-position: -140px -220px;
+}
+.emoji-bomb {
+ background-position: -160px -220px;
+}
+.emoji-book {
+ background-position: -180px -220px;
+}
+.emoji-bookmark {
+ background-position: -200px -220px;
+}
+.emoji-bookmark_tabs {
+ background-position: -220px -220px;
+}
+.emoji-books {
+ background-position: -240px 0;
+}
+.emoji-boom {
+ background-position: -240px -20px;
+}
+.emoji-boot {
+ background-position: -240px -40px;
+}
+.emoji-bouquet {
+ background-position: -240px -60px;
+}
+.emoji-bow {
+ background-position: -240px -80px;
+}
+.emoji-bow_and_arrow {
+ background-position: -240px -100px;
+}
+.emoji-bow_tone1 {
+ background-position: -240px -120px;
+}
+.emoji-bow_tone2 {
+ background-position: -240px -140px;
+}
+.emoji-bow_tone3 {
+ background-position: -240px -160px;
+}
+.emoji-bow_tone4 {
+ background-position: -240px -180px;
+}
+.emoji-bow_tone5 {
+ background-position: -240px -200px;
+}
+.emoji-bowling {
+ background-position: -240px -220px;
+}
+.emoji-boxing_glove {
+ background-position: 0 -240px;
+}
+.emoji-boy {
+ background-position: -20px -240px;
+}
+.emoji-boy_tone1 {
+ background-position: -40px -240px;
+}
+.emoji-boy_tone2 {
+ background-position: -60px -240px;
+}
+.emoji-boy_tone3 {
+ background-position: -80px -240px;
+}
+.emoji-boy_tone4 {
+ background-position: -100px -240px;
+}
+.emoji-boy_tone5 {
+ background-position: -120px -240px;
+}
+.emoji-bread {
+ background-position: -140px -240px;
+}
+.emoji-bride_with_veil {
+ background-position: -160px -240px;
+}
+.emoji-bride_with_veil_tone1 {
+ background-position: -180px -240px;
+}
+.emoji-bride_with_veil_tone2 {
+ background-position: -200px -240px;
+}
+.emoji-bride_with_veil_tone3 {
+ background-position: -220px -240px;
+}
+.emoji-bride_with_veil_tone4 {
+ background-position: -240px -240px;
+}
+.emoji-bride_with_veil_tone5 {
+ background-position: -260px 0;
+}
+.emoji-bridge_at_night {
+ background-position: -260px -20px;
+}
+.emoji-briefcase {
+ background-position: -260px -40px;
+}
+.emoji-broken_heart {
+ background-position: -260px -60px;
+}
+.emoji-bug {
+ background-position: -260px -80px;
+}
+.emoji-bulb {
+ background-position: -260px -100px;
+}
+.emoji-bullettrain_front {
+ background-position: -260px -120px;
+}
+.emoji-bullettrain_side {
+ background-position: -260px -140px;
+}
+.emoji-burrito {
+ background-position: -260px -160px;
+}
+.emoji-bus {
+ background-position: -260px -180px;
+}
+.emoji-busstop {
+ background-position: -260px -200px;
+}
+.emoji-bust_in_silhouette {
+ background-position: -260px -220px;
+}
+.emoji-busts_in_silhouette {
+ background-position: -260px -240px;
+}
+.emoji-butterfly {
+ background-position: 0 -260px;
+}
+.emoji-cactus {
+ background-position: -20px -260px;
+}
+.emoji-cake {
+ background-position: -40px -260px;
+}
+.emoji-calendar {
+ background-position: -60px -260px;
+}
+.emoji-calendar_spiral {
+ background-position: -80px -260px;
+}
+.emoji-call_me {
+ background-position: -100px -260px;
+}
+.emoji-call_me_tone1 {
+ background-position: -120px -260px;
+}
+.emoji-call_me_tone2 {
+ background-position: -140px -260px;
+}
+.emoji-call_me_tone3 {
+ background-position: -160px -260px;
+}
+.emoji-call_me_tone4 {
+ background-position: -180px -260px;
+}
+.emoji-call_me_tone5 {
+ background-position: -200px -260px;
+}
+.emoji-calling {
+ background-position: -220px -260px;
+}
+.emoji-camel {
+ background-position: -240px -260px;
+}
+.emoji-camera {
+ background-position: -260px -260px;
+}
+.emoji-camera_with_flash {
+ background-position: -280px 0;
+}
+.emoji-camping {
+ background-position: -280px -20px;
+}
+.emoji-cancer {
+ background-position: -280px -40px;
+}
+.emoji-candle {
+ background-position: -280px -60px;
+}
+.emoji-candy {
+ background-position: -280px -80px;
+}
+.emoji-canoe {
+ background-position: -280px -100px;
+}
+.emoji-capital_abcd {
+ background-position: -280px -120px;
+}
+.emoji-capricorn {
+ background-position: -280px -140px;
+}
+.emoji-card_box {
+ background-position: -280px -160px;
+}
+.emoji-card_index {
+ background-position: -280px -180px;
+}
+.emoji-carousel_horse {
+ background-position: -280px -200px;
+}
+.emoji-carrot {
+ background-position: -280px -220px;
+}
+.emoji-cartwheel {
+ background-position: -280px -240px;
+}
+.emoji-cartwheel_tone1 {
+ background-position: -280px -260px;
+}
+.emoji-cartwheel_tone2 {
+ background-position: 0 -280px;
+}
+.emoji-cartwheel_tone3 {
+ background-position: -20px -280px;
+}
+.emoji-cartwheel_tone4 {
+ background-position: -40px -280px;
+}
+.emoji-cartwheel_tone5 {
+ background-position: -60px -280px;
+}
+.emoji-cat {
+ background-position: -80px -280px;
+}
+.emoji-cat2 {
+ background-position: -100px -280px;
+}
+.emoji-cd {
+ background-position: -120px -280px;
+}
+.emoji-chains {
+ background-position: -140px -280px;
+}
+.emoji-champagne {
+ background-position: -160px -280px;
+}
+.emoji-champagne_glass {
+ background-position: -180px -280px;
+}
+.emoji-chart {
+ background-position: -200px -280px;
+}
+.emoji-chart_with_downwards_trend {
+ background-position: -220px -280px;
+}
+.emoji-chart_with_upwards_trend {
+ background-position: -240px -280px;
+}
+.emoji-checkered_flag {
+ background-position: -260px -280px;
+}
+.emoji-cheese {
+ background-position: -280px -280px;
+}
+.emoji-cherries {
+ background-position: -300px 0;
+}
+.emoji-cherry_blossom {
+ background-position: -300px -20px;
+}
+.emoji-chestnut {
+ background-position: -300px -40px;
+}
+.emoji-chicken {
+ background-position: -300px -60px;
+}
+.emoji-children_crossing {
+ background-position: -300px -80px;
+}
+.emoji-chipmunk {
+ background-position: -300px -100px;
+}
+.emoji-chocolate_bar {
+ background-position: -300px -120px;
+}
+.emoji-christmas_tree {
+ background-position: -300px -140px;
+}
+.emoji-church {
+ background-position: -300px -160px;
+}
+.emoji-cinema {
+ background-position: -300px -180px;
+}
+.emoji-circus_tent {
+ background-position: -300px -200px;
+}
+.emoji-city_dusk {
+ background-position: -300px -220px;
+}
+.emoji-city_sunset {
+ background-position: -300px -240px;
+}
+.emoji-cityscape {
+ background-position: -300px -260px;
+}
+.emoji-cl {
+ background-position: -300px -280px;
+}
+.emoji-clap {
+ background-position: 0 -300px;
+}
+.emoji-clap_tone1 {
+ background-position: -20px -300px;
+}
+.emoji-clap_tone2 {
+ background-position: -40px -300px;
+}
+.emoji-clap_tone3 {
+ background-position: -60px -300px;
+}
+.emoji-clap_tone4 {
+ background-position: -80px -300px;
+}
+.emoji-clap_tone5 {
+ background-position: -100px -300px;
+}
+.emoji-clapper {
+ background-position: -120px -300px;
+}
+.emoji-classical_building {
+ background-position: -140px -300px;
+}
+.emoji-clipboard {
+ background-position: -160px -300px;
+}
+.emoji-clock {
+ background-position: -180px -300px;
+}
+.emoji-clock1 {
+ background-position: -200px -300px;
+}
+.emoji-clock10 {
+ background-position: -220px -300px;
+}
+.emoji-clock1030 {
+ background-position: -240px -300px;
+}
+.emoji-clock11 {
+ background-position: -260px -300px;
+}
+.emoji-clock1130 {
+ background-position: -280px -300px;
+}
+.emoji-clock12 {
+ background-position: -300px -300px;
+}
+.emoji-clock1230 {
+ background-position: -320px 0;
+}
+.emoji-clock130 {
+ background-position: -320px -20px;
+}
+.emoji-clock2 {
+ background-position: -320px -40px;
+}
+.emoji-clock230 {
+ background-position: -320px -60px;
+}
+.emoji-clock3 {
+ background-position: -320px -80px;
+}
+.emoji-clock330 {
+ background-position: -320px -100px;
+}
+.emoji-clock4 {
+ background-position: -320px -120px;
+}
+.emoji-clock430 {
+ background-position: -320px -140px;
+}
+.emoji-clock5 {
+ background-position: -320px -160px;
+}
+.emoji-clock530 {
+ background-position: -320px -180px;
+}
+.emoji-clock6 {
+ background-position: -320px -200px;
+}
+.emoji-clock630 {
+ background-position: -320px -220px;
+}
+.emoji-clock7 {
+ background-position: -320px -240px;
+}
+.emoji-clock730 {
+ background-position: -320px -260px;
+}
+.emoji-clock8 {
+ background-position: -320px -280px;
+}
+.emoji-clock830 {
+ background-position: -320px -300px;
+}
+.emoji-clock9 {
+ background-position: 0 -320px;
+}
+.emoji-clock930 {
+ background-position: -20px -320px;
+}
+.emoji-closed_book {
+ background-position: -40px -320px;
+}
+.emoji-closed_lock_with_key {
+ background-position: -60px -320px;
+}
+.emoji-closed_umbrella {
+ background-position: -80px -320px;
+}
+.emoji-cloud {
+ background-position: -100px -320px;
+}
+.emoji-cloud_lightning {
+ background-position: -120px -320px;
+}
+.emoji-cloud_rain {
+ background-position: -140px -320px;
+}
+.emoji-cloud_snow {
+ background-position: -160px -320px;
+}
+.emoji-cloud_tornado {
+ background-position: -180px -320px;
+}
+.emoji-clown {
+ background-position: -200px -320px;
+}
+.emoji-clubs {
+ background-position: -220px -320px;
+}
+.emoji-cocktail {
+ background-position: -240px -320px;
+}
+.emoji-coffee {
+ background-position: -260px -320px;
+}
+.emoji-coffin {
+ background-position: -280px -320px;
+}
+.emoji-cold_sweat {
+ background-position: -300px -320px;
+}
+.emoji-comet {
+ background-position: -320px -320px;
+}
+.emoji-compression {
+ background-position: -340px 0;
+}
+.emoji-computer {
+ background-position: -340px -20px;
+}
+.emoji-confetti_ball {
+ background-position: -340px -40px;
+}
+.emoji-confounded {
+ background-position: -340px -60px;
+}
+.emoji-confused {
+ background-position: -340px -80px;
+}
+.emoji-congratulations {
+ background-position: -340px -100px;
+}
+.emoji-construction {
+ background-position: -340px -120px;
+}
+.emoji-construction_site {
+ background-position: -340px -140px;
+}
+.emoji-construction_worker {
+ background-position: -340px -160px;
+}
+.emoji-construction_worker_tone1 {
+ background-position: -340px -180px;
+}
+.emoji-construction_worker_tone2 {
+ background-position: -340px -200px;
+}
+.emoji-construction_worker_tone3 {
+ background-position: -340px -220px;
+}
+.emoji-construction_worker_tone4 {
+ background-position: -340px -240px;
+}
+.emoji-construction_worker_tone5 {
+ background-position: -340px -260px;
+}
+.emoji-control_knobs {
+ background-position: -340px -280px;
+}
+.emoji-convenience_store {
+ background-position: -340px -300px;
+}
+.emoji-cookie {
+ background-position: -340px -320px;
+}
+.emoji-cooking {
+ background-position: 0 -340px;
+}
+.emoji-cool {
+ background-position: -20px -340px;
+}
+.emoji-cop {
+ background-position: -40px -340px;
+}
+.emoji-cop_tone1 {
+ background-position: -60px -340px;
+}
+.emoji-cop_tone2 {
+ background-position: -80px -340px;
+}
+.emoji-cop_tone3 {
+ background-position: -100px -340px;
+}
+.emoji-cop_tone4 {
+ background-position: -120px -340px;
+}
+.emoji-cop_tone5 {
+ background-position: -140px -340px;
+}
+.emoji-copyright {
+ background-position: -160px -340px;
+}
+.emoji-corn {
+ background-position: -180px -340px;
+}
+.emoji-couch {
+ background-position: -200px -340px;
+}
+.emoji-couple {
+ background-position: -220px -340px;
+}
+.emoji-couple_mm {
+ background-position: -240px -340px;
+}
+.emoji-couple_with_heart {
+ background-position: -260px -340px;
+}
+.emoji-couple_ww {
+ background-position: -280px -340px;
+}
+.emoji-couplekiss {
+ background-position: -300px -340px;
+}
+.emoji-cow {
+ background-position: -320px -340px;
+}
+.emoji-cow2 {
+ background-position: -340px -340px;
+}
+.emoji-cowboy {
+ background-position: -360px 0;
+}
+.emoji-crab {
+ background-position: -360px -20px;
+}
+.emoji-crayon {
+ background-position: -360px -40px;
+}
+.emoji-credit_card {
+ background-position: -360px -60px;
+}
+.emoji-crescent_moon {
+ background-position: -360px -80px;
+}
+.emoji-cricket {
+ background-position: -360px -100px;
+}
+.emoji-crocodile {
+ background-position: -360px -120px;
+}
+.emoji-croissant {
+ background-position: -360px -140px;
+}
+.emoji-cross {
+ background-position: -360px -160px;
+}
+.emoji-crossed_flags {
+ background-position: -360px -180px;
+}
+.emoji-crossed_swords {
+ background-position: -360px -200px;
+}
+.emoji-crown {
+ background-position: -360px -220px;
+}
+.emoji-cruise_ship {
+ background-position: -360px -240px;
+}
+.emoji-cry {
+ background-position: -360px -260px;
+}
+.emoji-crying_cat_face {
+ background-position: -360px -280px;
+}
+.emoji-crystal_ball {
+ background-position: -360px -300px;
+}
+.emoji-cucumber {
+ background-position: -360px -320px;
+}
+.emoji-cupid {
+ background-position: -360px -340px;
+}
+.emoji-curly_loop {
+ background-position: 0 -360px;
+}
+.emoji-currency_exchange {
+ background-position: -20px -360px;
+}
+.emoji-curry {
+ background-position: -40px -360px;
+}
+.emoji-custard {
+ background-position: -60px -360px;
+}
+.emoji-customs {
+ background-position: -80px -360px;
+}
+.emoji-cyclone {
+ background-position: -100px -360px;
+}
+.emoji-dagger {
+ background-position: -120px -360px;
+}
+.emoji-dancer {
+ background-position: -140px -360px;
+}
+.emoji-dancer_tone1 {
+ background-position: -160px -360px;
+}
+.emoji-dancer_tone2 {
+ background-position: -180px -360px;
+}
+.emoji-dancer_tone3 {
+ background-position: -200px -360px;
+}
+.emoji-dancer_tone4 {
+ background-position: -220px -360px;
+}
+.emoji-dancer_tone5 {
+ background-position: -240px -360px;
+}
+.emoji-dancers {
+ background-position: -260px -360px;
+}
+.emoji-dango {
+ background-position: -280px -360px;
+}
+.emoji-dark_sunglasses {
+ background-position: -300px -360px;
+}
+.emoji-dart {
+ background-position: -320px -360px;
+}
+.emoji-dash {
+ background-position: -340px -360px;
+}
+.emoji-date {
+ background-position: -360px -360px;
+}
+.emoji-deciduous_tree {
+ background-position: -380px 0;
+}
+.emoji-deer {
+ background-position: -380px -20px;
+}
+.emoji-department_store {
+ background-position: -380px -40px;
+}
+.emoji-desert {
+ background-position: -380px -60px;
+}
+.emoji-desktop {
+ background-position: -380px -80px;
+}
+.emoji-diamond_shape_with_a_dot_inside {
+ background-position: -380px -100px;
+}
+.emoji-diamonds {
+ background-position: -380px -120px;
+}
+.emoji-disappointed {
+ background-position: -380px -140px;
+}
+.emoji-disappointed_relieved {
+ background-position: -380px -160px;
+}
+.emoji-dividers {
+ background-position: -380px -180px;
+}
+.emoji-dizzy {
+ background-position: -380px -200px;
+}
+.emoji-dizzy_face {
+ background-position: -380px -220px;
+}
+.emoji-do_not_litter {
+ background-position: -380px -240px;
+}
+.emoji-dog {
+ background-position: -380px -260px;
+}
+.emoji-dog2 {
+ background-position: -380px -280px;
+}
+.emoji-dollar {
+ background-position: -380px -300px;
+}
+.emoji-dolls {
+ background-position: -380px -320px;
+}
+.emoji-dolphin {
+ background-position: -380px -340px;
+}
+.emoji-door {
+ background-position: -380px -360px;
+}
+.emoji-doughnut {
+ background-position: 0 -380px;
+}
+.emoji-dove {
+ background-position: -20px -380px;
+}
+.emoji-dragon {
+ background-position: -40px -380px;
+}
+.emoji-dragon_face {
+ background-position: -60px -380px;
+}
+.emoji-dress {
+ background-position: -80px -380px;
+}
+.emoji-dromedary_camel {
+ background-position: -100px -380px;
+}
+.emoji-drooling_face {
+ background-position: -120px -380px;
+}
+.emoji-droplet {
+ background-position: -140px -380px;
+}
+.emoji-drum {
+ background-position: -160px -380px;
+}
+.emoji-duck {
+ background-position: -180px -380px;
+}
+.emoji-dvd {
+ background-position: -200px -380px;
+}
+.emoji-e-mail {
+ background-position: -220px -380px;
+}
+.emoji-eagle {
+ background-position: -240px -380px;
+}
+.emoji-ear {
+ background-position: -260px -380px;
+}
+.emoji-ear_of_rice {
+ background-position: -280px -380px;
+}
+.emoji-ear_tone1 {
+ background-position: -300px -380px;
+}
+.emoji-ear_tone2 {
+ background-position: -320px -380px;
+}
+.emoji-ear_tone3 {
+ background-position: -340px -380px;
+}
+.emoji-ear_tone4 {
+ background-position: -360px -380px;
+}
+.emoji-ear_tone5 {
+ background-position: -380px -380px;
+}
+.emoji-earth_africa {
+ background-position: -400px 0;
+}
+.emoji-earth_americas {
+ background-position: -400px -20px;
+}
+.emoji-earth_asia {
+ background-position: -400px -40px;
+}
+.emoji-egg {
+ background-position: -400px -60px;
+}
+.emoji-eggplant {
+ background-position: -400px -80px;
+}
+.emoji-eight {
+ background-position: -400px -100px;
+}
+.emoji-eight_pointed_black_star {
+ background-position: -400px -120px;
+}
+.emoji-eight_spoked_asterisk {
+ background-position: -400px -140px;
+}
+.emoji-eject {
+ background-position: -400px -160px;
+}
+.emoji-electric_plug {
+ background-position: -400px -180px;
+}
+.emoji-elephant {
+ background-position: -400px -200px;
+}
+.emoji-end {
+ background-position: -400px -220px;
+}
+.emoji-envelope {
+ background-position: -400px -240px;
+}
+.emoji-envelope_with_arrow {
+ background-position: -400px -260px;
+}
+.emoji-euro {
+ background-position: -400px -280px;
+}
+.emoji-european_castle {
+ background-position: -400px -300px;
+}
+.emoji-european_post_office {
+ background-position: -400px -320px;
+}
+.emoji-evergreen_tree {
+ background-position: -400px -340px;
+}
+.emoji-exclamation {
+ background-position: -400px -360px;
+}
+.emoji-expressionless {
+ background-position: -400px -380px;
+}
+.emoji-eye {
+ background-position: 0 -400px;
+}
+.emoji-eye_in_speech_bubble {
+ background-position: -20px -400px;
+}
+.emoji-eyeglasses {
+ background-position: -40px -400px;
+}
+.emoji-eyes {
+ background-position: -60px -400px;
+}
+.emoji-face_palm {
+ background-position: -80px -400px;
+}
+.emoji-face_palm_tone1 {
+ background-position: -100px -400px;
+}
+.emoji-face_palm_tone2 {
+ background-position: -120px -400px;
+}
+.emoji-face_palm_tone3 {
+ background-position: -140px -400px;
+}
+.emoji-face_palm_tone4 {
+ background-position: -160px -400px;
+}
+.emoji-face_palm_tone5 {
+ background-position: -180px -400px;
+}
+.emoji-factory {
+ background-position: -200px -400px;
+}
+.emoji-fallen_leaf {
+ background-position: -220px -400px;
+}
+.emoji-family {
+ background-position: -240px -400px;
+}
+.emoji-family_mmb {
+ background-position: -260px -400px;
+}
+.emoji-family_mmbb {
+ background-position: -280px -400px;
+}
+.emoji-family_mmg {
+ background-position: -300px -400px;
+}
+.emoji-family_mmgb {
+ background-position: -320px -400px;
+}
+.emoji-family_mmgg {
+ background-position: -340px -400px;
+}
+.emoji-family_mwbb {
+ background-position: -360px -400px;
+}
+.emoji-family_mwg {
+ background-position: -380px -400px;
+}
+.emoji-family_mwgb {
+ background-position: -400px -400px;
+}
+.emoji-family_mwgg {
+ background-position: -420px 0;
+}
+.emoji-family_wwb {
+ background-position: -420px -20px;
+}
+.emoji-family_wwbb {
+ background-position: -420px -40px;
+}
+.emoji-family_wwg {
+ background-position: -420px -60px;
+}
+.emoji-family_wwgb {
+ background-position: -420px -80px;
+}
+.emoji-family_wwgg {
+ background-position: -420px -100px;
+}
+.emoji-fast_forward {
+ background-position: -420px -120px;
+}
+.emoji-fax {
+ background-position: -420px -140px;
+}
+.emoji-fearful {
+ background-position: -420px -160px;
+}
+.emoji-feet {
+ background-position: -420px -180px;
+}
+.emoji-fencer {
+ background-position: -420px -200px;
+}
+.emoji-ferris_wheel {
+ background-position: -420px -220px;
+}
+.emoji-ferry {
+ background-position: -420px -240px;
+}
+.emoji-field_hockey {
+ background-position: -420px -260px;
+}
+.emoji-file_cabinet {
+ background-position: -420px -280px;
+}
+.emoji-file_folder {
+ background-position: -420px -300px;
+}
+.emoji-film_frames {
+ background-position: -420px -320px;
+}
+.emoji-fingers_crossed {
+ background-position: -420px -340px;
+}
+.emoji-fingers_crossed_tone1 {
+ background-position: -420px -360px;
+}
+.emoji-fingers_crossed_tone2 {
+ background-position: -420px -380px;
+}
+.emoji-fingers_crossed_tone3 {
+ background-position: -420px -400px;
+}
+.emoji-fingers_crossed_tone4 {
+ background-position: 0 -420px;
+}
+.emoji-fingers_crossed_tone5 {
+ background-position: -20px -420px;
+}
+.emoji-fire {
+ background-position: -40px -420px;
+}
+.emoji-fire_engine {
+ background-position: -60px -420px;
+}
+.emoji-fireworks {
+ background-position: -80px -420px;
+}
+.emoji-first_place {
+ background-position: -100px -420px;
+}
+.emoji-first_quarter_moon {
+ background-position: -120px -420px;
+}
+.emoji-first_quarter_moon_with_face {
+ background-position: -140px -420px;
+}
+.emoji-fish {
+ background-position: -160px -420px;
+}
+.emoji-fish_cake {
+ background-position: -180px -420px;
+}
+.emoji-fishing_pole_and_fish {
+ background-position: -200px -420px;
+}
+.emoji-fist {
+ background-position: -220px -420px;
+}
+.emoji-fist_tone1 {
+ background-position: -240px -420px;
+}
+.emoji-fist_tone2 {
+ background-position: -260px -420px;
+}
+.emoji-fist_tone3 {
+ background-position: -280px -420px;
+}
+.emoji-fist_tone4 {
+ background-position: -300px -420px;
+}
+.emoji-fist_tone5 {
+ background-position: -320px -420px;
+}
+.emoji-five {
+ background-position: -340px -420px;
+}
+.emoji-flag_ac {
+ background-position: -360px -420px;
+}
+.emoji-flag_ad {
+ background-position: -380px -420px;
+}
+.emoji-flag_ae {
+ background-position: -400px -420px;
+}
+.emoji-flag_af {
+ background-position: -420px -420px;
+}
+.emoji-flag_ag {
+ background-position: -440px 0;
+}
+.emoji-flag_ai {
+ background-position: -440px -20px;
+}
+.emoji-flag_al {
+ background-position: -440px -40px;
+}
+.emoji-flag_am {
+ background-position: -440px -60px;
+}
+.emoji-flag_ao {
+ background-position: -440px -80px;
+}
+.emoji-flag_aq {
+ background-position: -440px -100px;
+}
+.emoji-flag_ar {
+ background-position: -440px -120px;
+}
+.emoji-flag_as {
+ background-position: -440px -140px;
+}
+.emoji-flag_at {
+ background-position: -440px -160px;
+}
+.emoji-flag_au {
+ background-position: -440px -180px;
+}
+.emoji-flag_aw {
+ background-position: -440px -200px;
+}
+.emoji-flag_ax {
+ background-position: -440px -220px;
+}
+.emoji-flag_az {
+ background-position: -440px -240px;
+}
+.emoji-flag_ba {
+ background-position: -440px -260px;
+}
+.emoji-flag_bb {
+ background-position: -440px -280px;
+}
+.emoji-flag_bd {
+ background-position: -440px -300px;
+}
+.emoji-flag_be {
+ background-position: -440px -320px;
+}
+.emoji-flag_bf {
+ background-position: -440px -340px;
+}
+.emoji-flag_bg {
+ background-position: -440px -360px;
+}
+.emoji-flag_bh {
+ background-position: -440px -380px;
+}
+.emoji-flag_bi {
+ background-position: -440px -400px;
+}
+.emoji-flag_bj {
+ background-position: -440px -420px;
+}
+.emoji-flag_bl {
+ background-position: 0 -440px;
+}
+.emoji-flag_black {
+ background-position: -20px -440px;
+}
+.emoji-flag_bm {
+ background-position: -40px -440px;
+}
+.emoji-flag_bn {
+ background-position: -60px -440px;
+}
+.emoji-flag_bo {
+ background-position: -80px -440px;
+}
+.emoji-flag_bq {
+ background-position: -100px -440px;
+}
+.emoji-flag_br {
+ background-position: -120px -440px;
+}
+.emoji-flag_bs {
+ background-position: -140px -440px;
+}
+.emoji-flag_bt {
+ background-position: -160px -440px;
+}
+.emoji-flag_bv {
+ background-position: -180px -440px;
+}
+.emoji-flag_bw {
+ background-position: -200px -440px;
+}
+.emoji-flag_by {
+ background-position: -220px -440px;
+}
+.emoji-flag_bz {
+ background-position: -240px -440px;
+}
+.emoji-flag_ca {
+ background-position: -260px -440px;
+}
+.emoji-flag_cc {
+ background-position: -280px -440px;
+}
+.emoji-flag_cd {
+ background-position: -300px -440px;
+}
+.emoji-flag_cf {
+ background-position: -320px -440px;
+}
+.emoji-flag_cg {
+ background-position: -340px -440px;
+}
+.emoji-flag_ch {
+ background-position: -360px -440px;
+}
+.emoji-flag_ci {
+ background-position: -380px -440px;
+}
+.emoji-flag_ck {
+ background-position: -400px -440px;
+}
+.emoji-flag_cl {
+ background-position: -420px -440px;
+}
+.emoji-flag_cm {
+ background-position: -440px -440px;
+}
+.emoji-flag_cn {
+ background-position: -460px 0;
+}
+.emoji-flag_co {
+ background-position: -460px -20px;
+}
+.emoji-flag_cp {
+ background-position: -460px -40px;
+}
+.emoji-flag_cr {
+ background-position: -460px -60px;
+}
+.emoji-flag_cu {
+ background-position: -460px -80px;
+}
+.emoji-flag_cv {
+ background-position: -460px -100px;
+}
+.emoji-flag_cw {
+ background-position: -460px -120px;
+}
+.emoji-flag_cx {
+ background-position: -460px -140px;
+}
+.emoji-flag_cy {
+ background-position: -460px -160px;
+}
+.emoji-flag_cz {
+ background-position: -460px -180px;
+}
+.emoji-flag_de {
+ background-position: -460px -200px;
+}
+.emoji-flag_dg {
+ background-position: -460px -220px;
+}
+.emoji-flag_dj {
+ background-position: -460px -240px;
+}
+.emoji-flag_dk {
+ background-position: -460px -260px;
+}
+.emoji-flag_dm {
+ background-position: -460px -280px;
+}
+.emoji-flag_do {
+ background-position: -460px -300px;
+}
+.emoji-flag_dz {
+ background-position: -460px -320px;
+}
+.emoji-flag_ea {
+ background-position: -460px -340px;
+}
+.emoji-flag_ec {
+ background-position: -460px -360px;
+}
+.emoji-flag_ee {
+ background-position: -460px -380px;
+}
+.emoji-flag_eg {
+ background-position: -460px -400px;
+}
+.emoji-flag_eh {
+ background-position: -460px -420px;
+}
+.emoji-flag_er {
+ background-position: -460px -440px;
+}
+.emoji-flag_es {
+ background-position: 0 -460px;
+}
+.emoji-flag_et {
+ background-position: -20px -460px;
+}
+.emoji-flag_eu {
+ background-position: -40px -460px;
+}
+.emoji-flag_fi {
+ background-position: -60px -460px;
+}
+.emoji-flag_fj {
+ background-position: -80px -460px;
+}
+.emoji-flag_fk {
+ background-position: -100px -460px;
+}
+.emoji-flag_fm {
+ background-position: -120px -460px;
+}
+.emoji-flag_fo {
+ background-position: -140px -460px;
+}
+.emoji-flag_fr {
+ background-position: -160px -460px;
+}
+.emoji-flag_ga {
+ background-position: -180px -460px;
+}
+.emoji-flag_gb {
+ background-position: -200px -460px;
+}
+.emoji-flag_gd {
+ background-position: -220px -460px;
+}
+.emoji-flag_ge {
+ background-position: -240px -460px;
+}
+.emoji-flag_gf {
+ background-position: -260px -460px;
+}
+.emoji-flag_gg {
+ background-position: -280px -460px;
+}
+.emoji-flag_gh {
+ background-position: -300px -460px;
+}
+.emoji-flag_gi {
+ background-position: -320px -460px;
+}
+.emoji-flag_gl {
+ background-position: -340px -460px;
+}
+.emoji-flag_gm {
+ background-position: -360px -460px;
+}
+.emoji-flag_gn {
+ background-position: -380px -460px;
+}
+.emoji-flag_gp {
+ background-position: -400px -460px;
+}
+.emoji-flag_gq {
+ background-position: -420px -460px;
+}
+.emoji-flag_gr {
+ background-position: -440px -460px;
+}
+.emoji-flag_gs {
+ background-position: -460px -460px;
+}
+.emoji-flag_gt {
+ background-position: -480px 0;
+}
+.emoji-flag_gu {
+ background-position: -480px -20px;
+}
+.emoji-flag_gw {
+ background-position: -480px -40px;
+}
+.emoji-flag_gy {
+ background-position: -480px -60px;
+}
+.emoji-flag_hk {
+ background-position: -480px -80px;
+}
+.emoji-flag_hm {
+ background-position: -480px -100px;
+}
+.emoji-flag_hn {
+ background-position: -480px -120px;
+}
+.emoji-flag_hr {
+ background-position: -480px -140px;
+}
+.emoji-flag_ht {
+ background-position: -480px -160px;
+}
+.emoji-flag_hu {
+ background-position: -480px -180px;
+}
+.emoji-flag_ic {
+ background-position: -480px -200px;
+}
+.emoji-flag_id {
+ background-position: -480px -220px;
+}
+.emoji-flag_ie {
+ background-position: -480px -240px;
+}
+.emoji-flag_il {
+ background-position: -480px -260px;
+}
+.emoji-flag_im {
+ background-position: -480px -280px;
+}
+.emoji-flag_in {
+ background-position: -480px -300px;
+}
+.emoji-flag_io {
+ background-position: -480px -320px;
+}
+.emoji-flag_iq {
+ background-position: -480px -340px;
+}
+.emoji-flag_ir {
+ background-position: -480px -360px;
+}
+.emoji-flag_is {
+ background-position: -480px -380px;
+}
+.emoji-flag_it {
+ background-position: -480px -400px;
+}
+.emoji-flag_je {
+ background-position: -480px -420px;
+}
+.emoji-flag_jm {
+ background-position: -480px -440px;
+}
+.emoji-flag_jo {
+ background-position: -480px -460px;
+}
+.emoji-flag_jp {
+ background-position: 0 -480px;
+}
+.emoji-flag_ke {
+ background-position: -20px -480px;
+}
+.emoji-flag_kg {
+ background-position: -40px -480px;
+}
+.emoji-flag_kh {
+ background-position: -60px -480px;
+}
+.emoji-flag_ki {
+ background-position: -80px -480px;
+}
+.emoji-flag_km {
+ background-position: -100px -480px;
+}
+.emoji-flag_kn {
+ background-position: -120px -480px;
+}
+.emoji-flag_kp {
+ background-position: -140px -480px;
+}
+.emoji-flag_kr {
+ background-position: -160px -480px;
+}
+.emoji-flag_kw {
+ background-position: -180px -480px;
+}
+.emoji-flag_ky {
+ background-position: -200px -480px;
+}
+.emoji-flag_kz {
+ background-position: -220px -480px;
+}
+.emoji-flag_la {
+ background-position: -240px -480px;
+}
+.emoji-flag_lb {
+ background-position: -260px -480px;
+}
+.emoji-flag_lc {
+ background-position: -280px -480px;
+}
+.emoji-flag_li {
+ background-position: -300px -480px;
+}
+.emoji-flag_lk {
+ background-position: -320px -480px;
+}
+.emoji-flag_lr {
+ background-position: -340px -480px;
+}
+.emoji-flag_ls {
+ background-position: -360px -480px;
+}
+.emoji-flag_lt {
+ background-position: -380px -480px;
+}
+.emoji-flag_lu {
+ background-position: -400px -480px;
+}
+.emoji-flag_lv {
+ background-position: -420px -480px;
+}
+.emoji-flag_ly {
+ background-position: -440px -480px;
+}
+.emoji-flag_ma {
+ background-position: -460px -480px;
+}
+.emoji-flag_mc {
+ background-position: -480px -480px;
+}
+.emoji-flag_md {
+ background-position: -500px 0;
+}
+.emoji-flag_me {
+ background-position: -500px -20px;
+}
+.emoji-flag_mf {
+ background-position: -500px -40px;
+}
+.emoji-flag_mg {
+ background-position: -500px -60px;
+}
+.emoji-flag_mh {
+ background-position: -500px -80px;
+}
+.emoji-flag_mk {
+ background-position: -500px -100px;
+}
+.emoji-flag_ml {
+ background-position: -500px -120px;
+}
+.emoji-flag_mm {
+ background-position: -500px -140px;
+}
+.emoji-flag_mn {
+ background-position: -500px -160px;
+}
+.emoji-flag_mo {
+ background-position: -500px -180px;
+}
+.emoji-flag_mp {
+ background-position: -500px -200px;
+}
+.emoji-flag_mq {
+ background-position: -500px -220px;
+}
+.emoji-flag_mr {
+ background-position: -500px -240px;
+}
+.emoji-flag_ms {
+ background-position: -500px -260px;
+}
+.emoji-flag_mt {
+ background-position: -500px -280px;
+}
+.emoji-flag_mu {
+ background-position: -500px -300px;
+}
+.emoji-flag_mv {
+ background-position: -500px -320px;
+}
+.emoji-flag_mw {
+ background-position: -500px -340px;
+}
+.emoji-flag_mx {
+ background-position: -500px -360px;
+}
+.emoji-flag_my {
+ background-position: -500px -380px;
+}
+.emoji-flag_mz {
+ background-position: -500px -400px;
+}
+.emoji-flag_na {
+ background-position: -500px -420px;
+}
+.emoji-flag_nc {
+ background-position: -500px -440px;
+}
+.emoji-flag_ne {
+ background-position: -500px -460px;
+}
+.emoji-flag_nf {
+ background-position: -500px -480px;
+}
+.emoji-flag_ng {
+ background-position: 0 -500px;
+}
+.emoji-flag_ni {
+ background-position: -20px -500px;
+}
+.emoji-flag_nl {
+ background-position: -40px -500px;
+}
+.emoji-flag_no {
+ background-position: -60px -500px;
+}
+.emoji-flag_np {
+ background-position: -80px -500px;
+}
+.emoji-flag_nr {
+ background-position: -100px -500px;
+}
+.emoji-flag_nu {
+ background-position: -120px -500px;
+}
+.emoji-flag_nz {
+ background-position: -140px -500px;
+}
+.emoji-flag_om {
+ background-position: -160px -500px;
+}
+.emoji-flag_pa {
+ background-position: -180px -500px;
+}
+.emoji-flag_pe {
+ background-position: -200px -500px;
+}
+.emoji-flag_pf {
+ background-position: -220px -500px;
+}
+.emoji-flag_pg {
+ background-position: -240px -500px;
+}
+.emoji-flag_ph {
+ background-position: -260px -500px;
+}
+.emoji-flag_pk {
+ background-position: -280px -500px;
+}
+.emoji-flag_pl {
+ background-position: -300px -500px;
+}
+.emoji-flag_pm {
+ background-position: -320px -500px;
+}
+.emoji-flag_pn {
+ background-position: -340px -500px;
+}
+.emoji-flag_pr {
+ background-position: -360px -500px;
+}
+.emoji-flag_ps {
+ background-position: -380px -500px;
+}
+.emoji-flag_pt {
+ background-position: -400px -500px;
+}
+.emoji-flag_pw {
+ background-position: -420px -500px;
+}
+.emoji-flag_py {
+ background-position: -440px -500px;
+}
+.emoji-flag_qa {
+ background-position: -460px -500px;
+}
+.emoji-flag_re {
+ background-position: -480px -500px;
+}
+.emoji-flag_ro {
+ background-position: -500px -500px;
+}
+.emoji-flag_rs {
+ background-position: -520px 0;
+}
+.emoji-flag_ru {
+ background-position: -520px -20px;
+}
+.emoji-flag_rw {
+ background-position: -520px -40px;
+}
+.emoji-flag_sa {
+ background-position: -520px -60px;
+}
+.emoji-flag_sb {
+ background-position: -520px -80px;
+}
+.emoji-flag_sc {
+ background-position: -520px -100px;
+}
+.emoji-flag_sd {
+ background-position: -520px -120px;
+}
+.emoji-flag_se {
+ background-position: -520px -140px;
+}
+.emoji-flag_sg {
+ background-position: -520px -160px;
+}
+.emoji-flag_sh {
+ background-position: -520px -180px;
+}
+.emoji-flag_si {
+ background-position: -520px -200px;
+}
+.emoji-flag_sj {
+ background-position: -520px -220px;
+}
+.emoji-flag_sk {
+ background-position: -520px -240px;
+}
+.emoji-flag_sl {
+ background-position: -520px -260px;
+}
+.emoji-flag_sm {
+ background-position: -520px -280px;
+}
+.emoji-flag_sn {
+ background-position: -520px -300px;
+}
+.emoji-flag_so {
+ background-position: -520px -320px;
+}
+.emoji-flag_sr {
+ background-position: -520px -340px;
+}
+.emoji-flag_ss {
+ background-position: -520px -360px;
+}
+.emoji-flag_st {
+ background-position: -520px -380px;
+}
+.emoji-flag_sv {
+ background-position: -520px -400px;
+}
+.emoji-flag_sx {
+ background-position: -520px -420px;
+}
+.emoji-flag_sy {
+ background-position: -520px -440px;
+}
+.emoji-flag_sz {
+ background-position: -520px -460px;
+}
+.emoji-flag_ta {
+ background-position: -520px -480px;
+}
+.emoji-flag_tc {
+ background-position: -520px -500px;
+}
+.emoji-flag_td {
+ background-position: 0 -520px;
+}
+.emoji-flag_tf {
+ background-position: -20px -520px;
+}
+.emoji-flag_tg {
+ background-position: -40px -520px;
+}
+.emoji-flag_th {
+ background-position: -60px -520px;
+}
+.emoji-flag_tj {
+ background-position: -80px -520px;
+}
+.emoji-flag_tk {
+ background-position: -100px -520px;
+}
+.emoji-flag_tl {
+ background-position: -120px -520px;
+}
+.emoji-flag_tm {
+ background-position: -140px -520px;
+}
+.emoji-flag_tn {
+ background-position: -160px -520px;
+}
+.emoji-flag_to {
+ background-position: -180px -520px;
+}
+.emoji-flag_tr {
+ background-position: -200px -520px;
+}
+.emoji-flag_tt {
+ background-position: -220px -520px;
+}
+.emoji-flag_tv {
+ background-position: -240px -520px;
+}
+.emoji-flag_tw {
+ background-position: -260px -520px;
+}
+.emoji-flag_tz {
+ background-position: -280px -520px;
+}
+.emoji-flag_ua {
+ background-position: -300px -520px;
+}
+.emoji-flag_ug {
+ background-position: -320px -520px;
+}
+.emoji-flag_um {
+ background-position: -340px -520px;
+}
+.emoji-flag_us {
+ background-position: -360px -520px;
+}
+.emoji-flag_uy {
+ background-position: -380px -520px;
+}
+.emoji-flag_uz {
+ background-position: -400px -520px;
+}
+.emoji-flag_va {
+ background-position: -420px -520px;
+}
+.emoji-flag_vc {
+ background-position: -440px -520px;
+}
+.emoji-flag_ve {
+ background-position: -460px -520px;
+}
+.emoji-flag_vg {
+ background-position: -480px -520px;
+}
+.emoji-flag_vi {
+ background-position: -500px -520px;
+}
+.emoji-flag_vn {
+ background-position: -520px -520px;
+}
+.emoji-flag_vu {
+ background-position: -540px 0;
+}
+.emoji-flag_wf {
+ background-position: -540px -20px;
+}
+.emoji-flag_white {
+ background-position: -540px -40px;
+}
+.emoji-flag_ws {
+ background-position: -540px -60px;
+}
+.emoji-flag_xk {
+ background-position: -540px -80px;
+}
+.emoji-flag_ye {
+ background-position: -540px -100px;
+}
+.emoji-flag_yt {
+ background-position: -540px -120px;
+}
+.emoji-flag_za {
+ background-position: -540px -140px;
+}
+.emoji-flag_zm {
+ background-position: -540px -160px;
+}
+.emoji-flag_zw {
+ background-position: -540px -180px;
+}
+.emoji-flags {
+ background-position: -540px -200px;
+}
+.emoji-flashlight {
+ background-position: -540px -220px;
+}
+.emoji-fleur-de-lis {
+ background-position: -540px -240px;
+}
+.emoji-floppy_disk {
+ background-position: -540px -260px;
+}
+.emoji-flower_playing_cards {
+ background-position: -540px -280px;
+}
+.emoji-flushed {
+ background-position: -540px -300px;
+}
+.emoji-fog {
+ background-position: -540px -320px;
+}
+.emoji-foggy {
+ background-position: -540px -340px;
+}
+.emoji-football {
+ background-position: -540px -360px;
+}
+.emoji-footprints {
+ background-position: -540px -380px;
+}
+.emoji-fork_and_knife {
+ background-position: -540px -400px;
+}
+.emoji-fork_knife_plate {
+ background-position: -540px -420px;
+}
+.emoji-fountain {
+ background-position: -540px -440px;
+}
+.emoji-four {
+ background-position: -540px -460px;
+}
+.emoji-four_leaf_clover {
+ background-position: -540px -480px;
+}
+.emoji-fox {
+ background-position: -540px -500px;
+}
+.emoji-frame_photo {
+ background-position: -540px -520px;
+}
+.emoji-free {
+ background-position: 0 -540px;
+}
+.emoji-french_bread {
+ background-position: -20px -540px;
+}
+.emoji-fried_shrimp {
+ background-position: -40px -540px;
+}
+.emoji-fries {
+ background-position: -60px -540px;
+}
+.emoji-frog {
+ background-position: -80px -540px;
+}
+.emoji-frowning {
+ background-position: -100px -540px;
+}
+.emoji-frowning2 {
+ background-position: -120px -540px;
+}
+.emoji-fuelpump {
+ background-position: -140px -540px;
+}
+.emoji-full_moon {
+ background-position: -160px -540px;
+}
+.emoji-full_moon_with_face {
+ background-position: -180px -540px;
+}
+.emoji-game_die {
+ background-position: -200px -540px;
+}
+.emoji-gay_pride_flag {
+ background-position: -220px -540px;
+}
+.emoji-gear {
+ background-position: -240px -540px;
+}
+.emoji-gem {
+ background-position: -260px -540px;
+}
+.emoji-gemini {
+ background-position: -280px -540px;
+}
+.emoji-ghost {
+ background-position: -300px -540px;
+}
+.emoji-gift {
+ background-position: -320px -540px;
+}
+.emoji-gift_heart {
+ background-position: -340px -540px;
+}
+.emoji-girl {
+ background-position: -360px -540px;
+}
+.emoji-girl_tone1 {
+ background-position: -380px -540px;
+}
+.emoji-girl_tone2 {
+ background-position: -400px -540px;
+}
+.emoji-girl_tone3 {
+ background-position: -420px -540px;
+}
+.emoji-girl_tone4 {
+ background-position: -440px -540px;
+}
+.emoji-girl_tone5 {
+ background-position: -460px -540px;
+}
+.emoji-globe_with_meridians {
+ background-position: -480px -540px;
+}
+.emoji-goal {
+ background-position: -500px -540px;
+}
+.emoji-goat {
+ background-position: -520px -540px;
+}
+.emoji-golf {
+ background-position: -540px -540px;
+}
+.emoji-golfer {
+ background-position: -560px 0;
+}
+.emoji-gorilla {
+ background-position: -560px -20px;
+}
+.emoji-grapes {
+ background-position: -560px -40px;
+}
+.emoji-green_apple {
+ background-position: -560px -60px;
+}
+.emoji-green_book {
+ background-position: -560px -80px;
+}
+.emoji-green_heart {
+ background-position: -560px -100px;
+}
+.emoji-grey_exclamation {
+ background-position: -560px -120px;
+}
+.emoji-grey_question {
+ background-position: -560px -140px;
+}
+.emoji-grimacing {
+ background-position: -560px -160px;
+}
+.emoji-grin {
+ background-position: -560px -180px;
+}
+.emoji-grinning {
+ background-position: -560px -200px;
+}
+.emoji-guardsman {
+ background-position: -560px -220px;
+}
+.emoji-guardsman_tone1 {
+ background-position: -560px -240px;
+}
+.emoji-guardsman_tone2 {
+ background-position: -560px -260px;
+}
+.emoji-guardsman_tone3 {
+ background-position: -560px -280px;
+}
+.emoji-guardsman_tone4 {
+ background-position: -560px -300px;
+}
+.emoji-guardsman_tone5 {
+ background-position: -560px -320px;
+}
+.emoji-guitar {
+ background-position: -560px -340px;
+}
+.emoji-gun {
+ background-position: -560px -360px;
+}
+.emoji-haircut {
+ background-position: -560px -380px;
+}
+.emoji-haircut_tone1 {
+ background-position: -560px -400px;
+}
+.emoji-haircut_tone2 {
+ background-position: -560px -420px;
+}
+.emoji-haircut_tone3 {
+ background-position: -560px -440px;
+}
+.emoji-haircut_tone4 {
+ background-position: -560px -460px;
+}
+.emoji-haircut_tone5 {
+ background-position: -560px -480px;
+}
+.emoji-hamburger {
+ background-position: -560px -500px;
+}
+.emoji-hammer {
+ background-position: -560px -520px;
+}
+.emoji-hammer_pick {
+ background-position: -560px -540px;
+}
+.emoji-hamster {
+ background-position: 0 -560px;
+}
+.emoji-hand_splayed {
+ background-position: -20px -560px;
+}
+.emoji-hand_splayed_tone1 {
+ background-position: -40px -560px;
+}
+.emoji-hand_splayed_tone2 {
+ background-position: -60px -560px;
+}
+.emoji-hand_splayed_tone3 {
+ background-position: -80px -560px;
+}
+.emoji-hand_splayed_tone4 {
+ background-position: -100px -560px;
+}
+.emoji-hand_splayed_tone5 {
+ background-position: -120px -560px;
+}
+.emoji-handbag {
+ background-position: -140px -560px;
+}
+.emoji-handball {
+ background-position: -160px -560px;
+}
+.emoji-handball_tone1 {
+ background-position: -180px -560px;
+}
+.emoji-handball_tone2 {
+ background-position: -200px -560px;
+}
+.emoji-handball_tone3 {
+ background-position: -220px -560px;
+}
+.emoji-handball_tone4 {
+ background-position: -240px -560px;
+}
+.emoji-handball_tone5 {
+ background-position: -260px -560px;
+}
+.emoji-handshake {
+ background-position: -280px -560px;
+}
+.emoji-handshake_tone1 {
+ background-position: -300px -560px;
+}
+.emoji-handshake_tone2 {
+ background-position: -320px -560px;
+}
+.emoji-handshake_tone3 {
+ background-position: -340px -560px;
+}
+.emoji-handshake_tone4 {
+ background-position: -360px -560px;
+}
+.emoji-handshake_tone5 {
+ background-position: -380px -560px;
+}
+.emoji-hash {
+ background-position: -400px -560px;
+}
+.emoji-hatched_chick {
+ background-position: -420px -560px;
+}
+.emoji-hatching_chick {
+ background-position: -440px -560px;
+}
+.emoji-head_bandage {
+ background-position: -460px -560px;
+}
+.emoji-headphones {
+ background-position: -480px -560px;
+}
+.emoji-hear_no_evil {
+ background-position: -500px -560px;
+}
+.emoji-heart {
+ background-position: -520px -560px;
+}
+.emoji-heart_decoration {
+ background-position: -540px -560px;
+}
+.emoji-heart_exclamation {
+ background-position: -560px -560px;
+}
+.emoji-heart_eyes {
+ background-position: -580px 0;
+}
+.emoji-heart_eyes_cat {
+ background-position: -580px -20px;
+}
+.emoji-heartbeat {
+ background-position: -580px -40px;
+}
+.emoji-heartpulse {
+ background-position: -580px -60px;
+}
+.emoji-hearts {
+ background-position: -580px -80px;
+}
+.emoji-heavy_check_mark {
+ background-position: -580px -100px;
+}
+.emoji-heavy_division_sign {
+ background-position: -580px -120px;
+}
+.emoji-heavy_dollar_sign {
+ background-position: -580px -140px;
+}
+.emoji-heavy_minus_sign {
+ background-position: -580px -160px;
+}
+.emoji-heavy_multiplication_x {
+ background-position: -580px -180px;
+}
+.emoji-heavy_plus_sign {
+ background-position: -580px -200px;
+}
+.emoji-helicopter {
+ background-position: -580px -220px;
+}
+.emoji-helmet_with_cross {
+ background-position: -580px -240px;
+}
+.emoji-herb {
+ background-position: -580px -260px;
+}
+.emoji-hibiscus {
+ background-position: -580px -280px;
+}
+.emoji-high_brightness {
+ background-position: -580px -300px;
+}
+.emoji-high_heel {
+ background-position: -580px -320px;
+}
+.emoji-hockey {
+ background-position: -580px -340px;
+}
+.emoji-hole {
+ background-position: -580px -360px;
+}
+.emoji-homes {
+ background-position: -580px -380px;
+}
+.emoji-honey_pot {
+ background-position: -580px -400px;
+}
+.emoji-horse {
+ background-position: -580px -420px;
+}
+.emoji-horse_racing {
+ background-position: -580px -440px;
+}
+.emoji-horse_racing_tone1 {
+ background-position: -580px -460px;
+}
+.emoji-horse_racing_tone2 {
+ background-position: -580px -480px;
+}
+.emoji-horse_racing_tone3 {
+ background-position: -580px -500px;
+}
+.emoji-horse_racing_tone4 {
+ background-position: -580px -520px;
+}
+.emoji-horse_racing_tone5 {
+ background-position: -580px -540px;
+}
+.emoji-hospital {
+ background-position: -580px -560px;
+}
+.emoji-hot_pepper {
+ background-position: 0 -580px;
+}
+.emoji-hotdog {
+ background-position: -20px -580px;
+}
+.emoji-hotel {
+ background-position: -40px -580px;
+}
+.emoji-hotsprings {
+ background-position: -60px -580px;
+}
+.emoji-hourglass {
+ background-position: -80px -580px;
+}
+.emoji-hourglass_flowing_sand {
+ background-position: -100px -580px;
+}
+.emoji-house {
+ background-position: -120px -580px;
+}
+.emoji-house_abandoned {
+ background-position: -140px -580px;
+}
+.emoji-house_with_garden {
+ background-position: -160px -580px;
+}
+.emoji-hugging {
+ background-position: -180px -580px;
+}
+.emoji-hushed {
+ background-position: -200px -580px;
+}
+.emoji-ice_cream {
+ background-position: -220px -580px;
+}
+.emoji-ice_skate {
+ background-position: -240px -580px;
+}
+.emoji-icecream {
+ background-position: -260px -580px;
+}
+.emoji-id {
+ background-position: -280px -580px;
+}
+.emoji-ideograph_advantage {
+ background-position: -300px -580px;
+}
+.emoji-imp {
+ background-position: -320px -580px;
+}
+.emoji-inbox_tray {
+ background-position: -340px -580px;
+}
+.emoji-incoming_envelope {
+ background-position: -360px -580px;
+}
+.emoji-information_desk_person {
+ background-position: -380px -580px;
+}
+.emoji-information_desk_person_tone1 {
+ background-position: -400px -580px;
+}
+.emoji-information_desk_person_tone2 {
+ background-position: -420px -580px;
+}
+.emoji-information_desk_person_tone3 {
+ background-position: -440px -580px;
+}
+.emoji-information_desk_person_tone4 {
+ background-position: -460px -580px;
+}
+.emoji-information_desk_person_tone5 {
+ background-position: -480px -580px;
+}
+.emoji-information_source {
+ background-position: -500px -580px;
+}
+.emoji-innocent {
+ background-position: -520px -580px;
+}
+.emoji-interrobang {
+ background-position: -540px -580px;
+}
+.emoji-iphone {
+ background-position: -560px -580px;
+}
+.emoji-island {
+ background-position: -580px -580px;
+}
+.emoji-izakaya_lantern {
+ background-position: -600px 0;
+}
+.emoji-jack_o_lantern {
+ background-position: -600px -20px;
+}
+.emoji-japan {
+ background-position: -600px -40px;
+}
+.emoji-japanese_castle {
+ background-position: -600px -60px;
+}
+.emoji-japanese_goblin {
+ background-position: -600px -80px;
+}
+.emoji-japanese_ogre {
+ background-position: -600px -100px;
+}
+.emoji-jeans {
+ background-position: -600px -120px;
+}
+.emoji-joy {
+ background-position: -600px -140px;
+}
+.emoji-joy_cat {
+ background-position: -600px -160px;
+}
+.emoji-joystick {
+ background-position: -600px -180px;
+}
+.emoji-juggling {
+ background-position: -600px -200px;
+}
+.emoji-juggling_tone1 {
+ background-position: -600px -220px;
+}
+.emoji-juggling_tone2 {
+ background-position: -600px -240px;
+}
+.emoji-juggling_tone3 {
+ background-position: -600px -260px;
+}
+.emoji-juggling_tone4 {
+ background-position: -600px -280px;
+}
+.emoji-juggling_tone5 {
+ background-position: -600px -300px;
+}
+.emoji-kaaba {
+ background-position: -600px -320px;
+}
+.emoji-key {
+ background-position: -600px -340px;
+}
+.emoji-key2 {
+ background-position: -600px -360px;
+}
+.emoji-keyboard {
+ background-position: -600px -380px;
+}
+.emoji-kimono {
+ background-position: -600px -400px;
+}
+.emoji-kiss {
+ background-position: -600px -420px;
+}
+.emoji-kiss_mm {
+ background-position: -600px -440px;
+}
+.emoji-kiss_ww {
+ background-position: -600px -460px;
+}
+.emoji-kissing {
+ background-position: -600px -480px;
+}
+.emoji-kissing_cat {
+ background-position: -600px -500px;
+}
+.emoji-kissing_closed_eyes {
+ background-position: -600px -520px;
+}
+.emoji-kissing_heart {
+ background-position: -600px -540px;
+}
+.emoji-kissing_smiling_eyes {
+ background-position: -600px -560px;
+}
+.emoji-kiwi {
+ background-position: -600px -580px;
+}
+.emoji-knife {
+ background-position: 0 -600px;
+}
+.emoji-koala {
+ background-position: -20px -600px;
+}
+.emoji-koko {
+ background-position: -40px -600px;
+}
+.emoji-label {
+ background-position: -60px -600px;
+}
+.emoji-large_blue_circle {
+ background-position: -80px -600px;
+}
+.emoji-large_blue_diamond {
+ background-position: -100px -600px;
+}
+.emoji-large_orange_diamond {
+ background-position: -120px -600px;
+}
+.emoji-last_quarter_moon {
+ background-position: -140px -600px;
+}
+.emoji-last_quarter_moon_with_face {
+ background-position: -160px -600px;
+}
+.emoji-laughing {
+ background-position: -180px -600px;
+}
+.emoji-leaves {
+ background-position: -200px -600px;
+}
+.emoji-ledger {
+ background-position: -220px -600px;
+}
+.emoji-left_facing_fist {
+ background-position: -240px -600px;
+}
+.emoji-left_facing_fist_tone1 {
+ background-position: -260px -600px;
+}
+.emoji-left_facing_fist_tone2 {
+ background-position: -280px -600px;
+}
+.emoji-left_facing_fist_tone3 {
+ background-position: -300px -600px;
+}
+.emoji-left_facing_fist_tone4 {
+ background-position: -320px -600px;
+}
+.emoji-left_facing_fist_tone5 {
+ background-position: -340px -600px;
+}
+.emoji-left_luggage {
+ background-position: -360px -600px;
+}
+.emoji-left_right_arrow {
+ background-position: -380px -600px;
+}
+.emoji-leftwards_arrow_with_hook {
+ background-position: -400px -600px;
+}
+.emoji-lemon {
+ background-position: -420px -600px;
+}
+.emoji-leo {
+ background-position: -440px -600px;
+}
+.emoji-leopard {
+ background-position: -460px -600px;
+}
+.emoji-level_slider {
+ background-position: -480px -600px;
+}
+.emoji-levitate {
+ background-position: -500px -600px;
+}
+.emoji-libra {
+ background-position: -520px -600px;
+}
+.emoji-lifter {
+ background-position: -540px -600px;
+}
+.emoji-lifter_tone1 {
+ background-position: -560px -600px;
+}
+.emoji-lifter_tone2 {
+ background-position: -580px -600px;
+}
+.emoji-lifter_tone3 {
+ background-position: -600px -600px;
+}
+.emoji-lifter_tone4 {
+ background-position: -620px 0;
+}
+.emoji-lifter_tone5 {
+ background-position: -620px -20px;
+}
+.emoji-light_rail {
+ background-position: -620px -40px;
+}
+.emoji-link {
+ background-position: -620px -60px;
+}
+.emoji-lion_face {
+ background-position: -620px -80px;
+}
+.emoji-lips {
+ background-position: -620px -100px;
+}
+.emoji-lipstick {
+ background-position: -620px -120px;
+}
+.emoji-lizard {
+ background-position: -620px -140px;
+}
+.emoji-lock {
+ background-position: -620px -160px;
+}
+.emoji-lock_with_ink_pen {
+ background-position: -620px -180px;
+}
+.emoji-lollipop {
+ background-position: -620px -200px;
+}
+.emoji-loop {
+ background-position: -620px -220px;
+}
+.emoji-loud_sound {
+ background-position: -620px -240px;
+}
+.emoji-loudspeaker {
+ background-position: -620px -260px;
+}
+.emoji-love_hotel {
+ background-position: -620px -280px;
+}
+.emoji-love_letter {
+ background-position: -620px -300px;
+}
+.emoji-low_brightness {
+ background-position: -620px -320px;
+}
+.emoji-lying_face {
+ background-position: -620px -340px;
+}
+.emoji-m {
+ background-position: -620px -360px;
+}
+.emoji-mag {
+ background-position: -620px -380px;
+}
+.emoji-mag_right {
+ background-position: -620px -400px;
+}
+.emoji-mahjong {
+ background-position: -620px -420px;
+}
+.emoji-mailbox {
+ background-position: -620px -440px;
+}
+.emoji-mailbox_closed {
+ background-position: -620px -460px;
+}
+.emoji-mailbox_with_mail {
+ background-position: -620px -480px;
+}
+.emoji-mailbox_with_no_mail {
+ background-position: -620px -500px;
+}
+.emoji-man {
+ background-position: -620px -520px;
+}
+.emoji-man_dancing {
+ background-position: -620px -540px;
+}
+.emoji-man_dancing_tone1 {
+ background-position: -620px -560px;
+}
+.emoji-man_dancing_tone2 {
+ background-position: -620px -580px;
+}
+.emoji-man_dancing_tone3 {
+ background-position: -620px -600px;
+}
+.emoji-man_dancing_tone4 {
+ background-position: 0 -620px;
+}
+.emoji-man_dancing_tone5 {
+ background-position: -20px -620px;
+}
+.emoji-man_in_tuxedo {
+ background-position: -40px -620px;
+}
+.emoji-man_in_tuxedo_tone1 {
+ background-position: -60px -620px;
+}
+.emoji-man_in_tuxedo_tone2 {
+ background-position: -80px -620px;
+}
+.emoji-man_in_tuxedo_tone3 {
+ background-position: -100px -620px;
+}
+.emoji-man_in_tuxedo_tone4 {
+ background-position: -120px -620px;
+}
+.emoji-man_in_tuxedo_tone5 {
+ background-position: -140px -620px;
+}
+.emoji-man_tone1 {
+ background-position: -160px -620px;
+}
+.emoji-man_tone2 {
+ background-position: -180px -620px;
+}
+.emoji-man_tone3 {
+ background-position: -200px -620px;
+}
+.emoji-man_tone4 {
+ background-position: -220px -620px;
+}
+.emoji-man_tone5 {
+ background-position: -240px -620px;
+}
+.emoji-man_with_gua_pi_mao {
+ background-position: -260px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone1 {
+ background-position: -280px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone2 {
+ background-position: -300px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone3 {
+ background-position: -320px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone4 {
+ background-position: -340px -620px;
+}
+.emoji-man_with_gua_pi_mao_tone5 {
+ background-position: -360px -620px;
+}
+.emoji-man_with_turban {
+ background-position: -380px -620px;
+}
+.emoji-man_with_turban_tone1 {
+ background-position: -400px -620px;
+}
+.emoji-man_with_turban_tone2 {
+ background-position: -420px -620px;
+}
+.emoji-man_with_turban_tone3 {
+ background-position: -440px -620px;
+}
+.emoji-man_with_turban_tone4 {
+ background-position: -460px -620px;
+}
+.emoji-man_with_turban_tone5 {
+ background-position: -480px -620px;
+}
+.emoji-mans_shoe {
+ background-position: -500px -620px;
+}
+.emoji-map {
+ background-position: -520px -620px;
+}
+.emoji-maple_leaf {
+ background-position: -540px -620px;
+}
+.emoji-martial_arts_uniform {
+ background-position: -560px -620px;
+}
+.emoji-mask {
+ background-position: -580px -620px;
+}
+.emoji-massage {
+ background-position: -600px -620px;
+}
+.emoji-massage_tone1 {
+ background-position: -620px -620px;
+}
+.emoji-massage_tone2 {
+ background-position: -640px 0;
+}
+.emoji-massage_tone3 {
+ background-position: -640px -20px;
+}
+.emoji-massage_tone4 {
+ background-position: -640px -40px;
+}
+.emoji-massage_tone5 {
+ background-position: -640px -60px;
+}
+.emoji-meat_on_bone {
+ background-position: -640px -80px;
+}
+.emoji-medal {
+ background-position: -640px -100px;
+}
+.emoji-mega {
+ background-position: -640px -120px;
+}
+.emoji-melon {
+ background-position: -640px -140px;
+}
+.emoji-menorah {
+ background-position: -640px -160px;
+}
+.emoji-mens {
+ background-position: -640px -180px;
+}
+.emoji-metal {
+ background-position: -640px -200px;
+}
+.emoji-metal_tone1 {
+ background-position: -640px -220px;
+}
+.emoji-metal_tone2 {
+ background-position: -640px -240px;
+}
+.emoji-metal_tone3 {
+ background-position: -640px -260px;
+}
+.emoji-metal_tone4 {
+ background-position: -640px -280px;
+}
+.emoji-metal_tone5 {
+ background-position: -640px -300px;
+}
+.emoji-metro {
+ background-position: -640px -320px;
+}
+.emoji-microphone {
+ background-position: -640px -340px;
+}
+.emoji-microphone2 {
+ background-position: -640px -360px;
+}
+.emoji-microscope {
+ background-position: -640px -380px;
+}
+.emoji-middle_finger {
+ background-position: -640px -400px;
+}
+.emoji-middle_finger_tone1 {
+ background-position: -640px -420px;
+}
+.emoji-middle_finger_tone2 {
+ background-position: -640px -440px;
+}
+.emoji-middle_finger_tone3 {
+ background-position: -640px -460px;
+}
+.emoji-middle_finger_tone4 {
+ background-position: -640px -480px;
+}
+.emoji-middle_finger_tone5 {
+ background-position: -640px -500px;
+}
+.emoji-military_medal {
+ background-position: -640px -520px;
+}
+.emoji-milk {
+ background-position: -640px -540px;
+}
+.emoji-milky_way {
+ background-position: -640px -560px;
+}
+.emoji-minibus {
+ background-position: -640px -580px;
+}
+.emoji-minidisc {
+ background-position: -640px -600px;
+}
+.emoji-mobile_phone_off {
+ background-position: -640px -620px;
+}
+.emoji-money_mouth {
+ background-position: 0 -640px;
+}
+.emoji-money_with_wings {
+ background-position: -20px -640px;
+}
+.emoji-moneybag {
+ background-position: -40px -640px;
+}
+.emoji-monkey {
+ background-position: -60px -640px;
+}
+.emoji-monkey_face {
+ background-position: -80px -640px;
+}
+.emoji-monorail {
+ background-position: -100px -640px;
+}
+.emoji-mortar_board {
+ background-position: -120px -640px;
+}
+.emoji-mosque {
+ background-position: -140px -640px;
+}
+.emoji-motor_scooter {
+ background-position: -160px -640px;
+}
+.emoji-motorboat {
+ background-position: -180px -640px;
+}
+.emoji-motorcycle {
+ background-position: -200px -640px;
+}
+.emoji-motorway {
+ background-position: -220px -640px;
+}
+.emoji-mount_fuji {
+ background-position: -240px -640px;
+}
+.emoji-mountain {
+ background-position: -260px -640px;
+}
+.emoji-mountain_bicyclist {
+ background-position: -280px -640px;
+}
+.emoji-mountain_bicyclist_tone1 {
+ background-position: -300px -640px;
+}
+.emoji-mountain_bicyclist_tone2 {
+ background-position: -320px -640px;
+}
+.emoji-mountain_bicyclist_tone3 {
+ background-position: -340px -640px;
+}
+.emoji-mountain_bicyclist_tone4 {
+ background-position: -360px -640px;
+}
+.emoji-mountain_bicyclist_tone5 {
+ background-position: -380px -640px;
+}
+.emoji-mountain_cableway {
+ background-position: -400px -640px;
+}
+.emoji-mountain_railway {
+ background-position: -420px -640px;
+}
+.emoji-mountain_snow {
+ background-position: -440px -640px;
+}
+.emoji-mouse {
+ background-position: -460px -640px;
+}
+.emoji-mouse2 {
+ background-position: -480px -640px;
+}
+.emoji-mouse_three_button {
+ background-position: -500px -640px;
+}
+.emoji-movie_camera {
+ background-position: -520px -640px;
+}
+.emoji-moyai {
+ background-position: -540px -640px;
+}
+.emoji-mrs_claus {
+ background-position: -560px -640px;
+}
+.emoji-mrs_claus_tone1 {
+ background-position: -580px -640px;
+}
+.emoji-mrs_claus_tone2 {
+ background-position: -600px -640px;
+}
+.emoji-mrs_claus_tone3 {
+ background-position: -620px -640px;
+}
+.emoji-mrs_claus_tone4 {
+ background-position: -640px -640px;
+}
+.emoji-mrs_claus_tone5 {
+ background-position: -660px 0;
+}
+.emoji-muscle {
+ background-position: -660px -20px;
+}
+.emoji-muscle_tone1 {
+ background-position: -660px -40px;
+}
+.emoji-muscle_tone2 {
+ background-position: -660px -60px;
+}
+.emoji-muscle_tone3 {
+ background-position: -660px -80px;
+}
+.emoji-muscle_tone4 {
+ background-position: -660px -100px;
+}
+.emoji-muscle_tone5 {
+ background-position: -660px -120px;
+}
+.emoji-mushroom {
+ background-position: -660px -140px;
+}
+.emoji-musical_keyboard {
+ background-position: -660px -160px;
+}
+.emoji-musical_note {
+ background-position: -660px -180px;
+}
+.emoji-musical_score {
+ background-position: -660px -200px;
+}
+.emoji-mute {
+ background-position: -660px -220px;
+}
+.emoji-nail_care {
+ background-position: -660px -240px;
+}
+.emoji-nail_care_tone1 {
+ background-position: -660px -260px;
+}
+.emoji-nail_care_tone2 {
+ background-position: -660px -280px;
+}
+.emoji-nail_care_tone3 {
+ background-position: -660px -300px;
+}
+.emoji-nail_care_tone4 {
+ background-position: -660px -320px;
+}
+.emoji-nail_care_tone5 {
+ background-position: -660px -340px;
+}
+.emoji-name_badge {
+ background-position: -660px -360px;
+}
+.emoji-nauseated_face {
+ background-position: -660px -380px;
+}
+.emoji-necktie {
+ background-position: -660px -400px;
+}
+.emoji-negative_squared_cross_mark {
+ background-position: -660px -420px;
+}
+.emoji-nerd {
+ background-position: -660px -440px;
+}
+.emoji-neutral_face {
+ background-position: -660px -460px;
+}
+.emoji-new {
+ background-position: -660px -480px;
+}
+.emoji-new_moon {
+ background-position: -660px -500px;
+}
+.emoji-new_moon_with_face {
+ background-position: -660px -520px;
+}
+.emoji-newspaper {
+ background-position: -660px -540px;
+}
+.emoji-newspaper2 {
+ background-position: -660px -560px;
+}
+.emoji-ng {
+ background-position: -660px -580px;
+}
+.emoji-night_with_stars {
+ background-position: -660px -600px;
+}
+.emoji-nine {
+ background-position: -660px -620px;
+}
+.emoji-no_bell {
+ background-position: -660px -640px;
+}
+.emoji-no_bicycles {
+ background-position: 0 -660px;
+}
+.emoji-no_entry {
+ background-position: -20px -660px;
+}
+.emoji-no_entry_sign {
+ background-position: -40px -660px;
+}
+.emoji-no_good {
+ background-position: -60px -660px;
+}
+.emoji-no_good_tone1 {
+ background-position: -80px -660px;
+}
+.emoji-no_good_tone2 {
+ background-position: -100px -660px;
+}
+.emoji-no_good_tone3 {
+ background-position: -120px -660px;
+}
+.emoji-no_good_tone4 {
+ background-position: -140px -660px;
+}
+.emoji-no_good_tone5 {
+ background-position: -160px -660px;
+}
+.emoji-no_mobile_phones {
+ background-position: -180px -660px;
+}
+.emoji-no_mouth {
+ background-position: -200px -660px;
+}
+.emoji-no_pedestrians {
+ background-position: -220px -660px;
+}
+.emoji-no_smoking {
+ background-position: -240px -660px;
+}
+.emoji-non-potable_water {
+ background-position: -260px -660px;
+}
+.emoji-nose {
+ background-position: -280px -660px;
+}
+.emoji-nose_tone1 {
+ background-position: -300px -660px;
+}
+.emoji-nose_tone2 {
+ background-position: -320px -660px;
+}
+.emoji-nose_tone3 {
+ background-position: -340px -660px;
+}
+.emoji-nose_tone4 {
+ background-position: -360px -660px;
+}
+.emoji-nose_tone5 {
+ background-position: -380px -660px;
+}
+.emoji-notebook {
+ background-position: -400px -660px;
+}
+.emoji-notebook_with_decorative_cover {
+ background-position: -420px -660px;
+}
+.emoji-notepad_spiral {
+ background-position: -440px -660px;
+}
+.emoji-notes {
+ background-position: -460px -660px;
+}
+.emoji-nut_and_bolt {
+ background-position: -480px -660px;
+}
+.emoji-o {
+ background-position: -500px -660px;
+}
+.emoji-o2 {
+ background-position: -520px -660px;
+}
+.emoji-ocean {
+ background-position: -540px -660px;
+}
+.emoji-octagonal_sign {
+ background-position: -560px -660px;
+}
+.emoji-octopus {
+ background-position: -580px -660px;
+}
+.emoji-oden {
+ background-position: -600px -660px;
+}
+.emoji-office {
+ background-position: -620px -660px;
+}
+.emoji-oil {
+ background-position: -640px -660px;
+}
+.emoji-ok {
+ background-position: -660px -660px;
+}
+.emoji-ok_hand {
+ background-position: -680px 0;
+}
+.emoji-ok_hand_tone1 {
+ background-position: -680px -20px;
+}
+.emoji-ok_hand_tone2 {
+ background-position: -680px -40px;
+}
+.emoji-ok_hand_tone3 {
+ background-position: -680px -60px;
+}
+.emoji-ok_hand_tone4 {
+ background-position: -680px -80px;
+}
+.emoji-ok_hand_tone5 {
+ background-position: -680px -100px;
+}
+.emoji-ok_woman {
+ background-position: -680px -120px;
+}
+.emoji-ok_woman_tone1 {
+ background-position: -680px -140px;
+}
+.emoji-ok_woman_tone2 {
+ background-position: -680px -160px;
+}
+.emoji-ok_woman_tone3 {
+ background-position: -680px -180px;
+}
+.emoji-ok_woman_tone4 {
+ background-position: -680px -200px;
+}
+.emoji-ok_woman_tone5 {
+ background-position: -680px -220px;
+}
+.emoji-older_man {
+ background-position: -680px -240px;
+}
+.emoji-older_man_tone1 {
+ background-position: -680px -260px;
+}
+.emoji-older_man_tone2 {
+ background-position: -680px -280px;
+}
+.emoji-older_man_tone3 {
+ background-position: -680px -300px;
+}
+.emoji-older_man_tone4 {
+ background-position: -680px -320px;
+}
+.emoji-older_man_tone5 {
+ background-position: -680px -340px;
+}
+.emoji-older_woman {
+ background-position: -680px -360px;
+}
+.emoji-older_woman_tone1 {
+ background-position: -680px -380px;
+}
+.emoji-older_woman_tone2 {
+ background-position: -680px -400px;
+}
+.emoji-older_woman_tone3 {
+ background-position: -680px -420px;
+}
+.emoji-older_woman_tone4 {
+ background-position: -680px -440px;
+}
+.emoji-older_woman_tone5 {
+ background-position: -680px -460px;
+}
+.emoji-om_symbol {
+ background-position: -680px -480px;
+}
+.emoji-on {
+ background-position: -680px -500px;
+}
+.emoji-oncoming_automobile {
+ background-position: -680px -520px;
+}
+.emoji-oncoming_bus {
+ background-position: -680px -540px;
+}
+.emoji-oncoming_police_car {
+ background-position: -680px -560px;
+}
+.emoji-oncoming_taxi {
+ background-position: -680px -580px;
+}
+.emoji-one {
+ background-position: -680px -600px;
+}
+.emoji-open_file_folder {
+ background-position: -680px -620px;
+}
+.emoji-open_hands {
+ background-position: -680px -640px;
+}
+.emoji-open_hands_tone1 {
+ background-position: -680px -660px;
+}
+.emoji-open_hands_tone2 {
+ background-position: 0 -680px;
+}
+.emoji-open_hands_tone3 {
+ background-position: -20px -680px;
+}
+.emoji-open_hands_tone4 {
+ background-position: -40px -680px;
+}
+.emoji-open_hands_tone5 {
+ background-position: -60px -680px;
+}
+.emoji-open_mouth {
+ background-position: -80px -680px;
+}
+.emoji-ophiuchus {
+ background-position: -100px -680px;
+}
+.emoji-orange_book {
+ background-position: -120px -680px;
+}
+.emoji-orthodox_cross {
+ background-position: -140px -680px;
+}
+.emoji-outbox_tray {
+ background-position: -160px -680px;
+}
+.emoji-owl {
+ background-position: -180px -680px;
+}
+.emoji-ox {
+ background-position: -200px -680px;
+}
+.emoji-package {
+ background-position: -220px -680px;
+}
+.emoji-page_facing_up {
+ background-position: -240px -680px;
+}
+.emoji-page_with_curl {
+ background-position: -260px -680px;
+}
+.emoji-pager {
+ background-position: -280px -680px;
+}
+.emoji-paintbrush {
+ background-position: -300px -680px;
+}
+.emoji-palm_tree {
+ background-position: -320px -680px;
+}
+.emoji-pancakes {
+ background-position: -340px -680px;
+}
+.emoji-panda_face {
+ background-position: -360px -680px;
+}
+.emoji-paperclip {
+ background-position: -380px -680px;
+}
+.emoji-paperclips {
+ background-position: -400px -680px;
+}
+.emoji-park {
+ background-position: -420px -680px;
+}
+.emoji-parking {
+ background-position: -440px -680px;
+}
+.emoji-part_alternation_mark {
+ background-position: -460px -680px;
+}
+.emoji-partly_sunny {
+ background-position: -480px -680px;
+}
+.emoji-passport_control {
+ background-position: -500px -680px;
+}
+.emoji-pause_button {
+ background-position: -520px -680px;
+}
+.emoji-peace {
+ background-position: -540px -680px;
+}
+.emoji-peach {
+ background-position: -560px -680px;
+}
+.emoji-peanuts {
+ background-position: -580px -680px;
+}
+.emoji-pear {
+ background-position: -600px -680px;
+}
+.emoji-pen_ballpoint {
+ background-position: -620px -680px;
+}
+.emoji-pen_fountain {
+ background-position: -640px -680px;
+}
+.emoji-pencil {
+ background-position: -660px -680px;
+}
+.emoji-pencil2 {
+ background-position: -680px -680px;
+}
+.emoji-penguin {
+ background-position: -700px 0;
+}
+.emoji-pensive {
+ background-position: -700px -20px;
+}
+.emoji-performing_arts {
+ background-position: -700px -40px;
+}
+.emoji-persevere {
+ background-position: -700px -60px;
+}
+.emoji-person_frowning {
+ background-position: -700px -80px;
+}
+.emoji-person_frowning_tone1 {
+ background-position: -700px -100px;
+}
+.emoji-person_frowning_tone2 {
+ background-position: -700px -120px;
+}
+.emoji-person_frowning_tone3 {
+ background-position: -700px -140px;
+}
+.emoji-person_frowning_tone4 {
+ background-position: -700px -160px;
+}
+.emoji-person_frowning_tone5 {
+ background-position: -700px -180px;
+}
+.emoji-person_with_blond_hair {
+ background-position: -700px -200px;
+}
+.emoji-person_with_blond_hair_tone1 {
+ background-position: -700px -220px;
+}
+.emoji-person_with_blond_hair_tone2 {
+ background-position: -700px -240px;
+}
+.emoji-person_with_blond_hair_tone3 {
+ background-position: -700px -260px;
+}
+.emoji-person_with_blond_hair_tone4 {
+ background-position: -700px -280px;
+}
+.emoji-person_with_blond_hair_tone5 {
+ background-position: -700px -300px;
+}
+.emoji-person_with_pouting_face {
+ background-position: -700px -320px;
+}
+.emoji-person_with_pouting_face_tone1 {
+ background-position: -700px -340px;
+}
+.emoji-person_with_pouting_face_tone2 {
+ background-position: -700px -360px;
+}
+.emoji-person_with_pouting_face_tone3 {
+ background-position: -700px -380px;
+}
+.emoji-person_with_pouting_face_tone4 {
+ background-position: -700px -400px;
+}
+.emoji-person_with_pouting_face_tone5 {
+ background-position: -700px -420px;
+}
+.emoji-pick {
+ background-position: -700px -440px;
+}
+.emoji-pig {
+ background-position: -700px -460px;
+}
+.emoji-pig2 {
+ background-position: -700px -480px;
+}
+.emoji-pig_nose {
+ background-position: -700px -500px;
+}
+.emoji-pill {
+ background-position: -700px -520px;
+}
+.emoji-pineapple {
+ background-position: -700px -540px;
+}
+.emoji-ping_pong {
+ background-position: -700px -560px;
+}
+.emoji-pisces {
+ background-position: -700px -580px;
+}
+.emoji-pizza {
+ background-position: -700px -600px;
+}
+.emoji-place_of_worship {
+ background-position: -700px -620px;
+}
+.emoji-play_pause {
+ background-position: -700px -640px;
+}
+.emoji-point_down {
+ background-position: -700px -660px;
+}
+.emoji-point_down_tone1 {
+ background-position: -700px -680px;
+}
+.emoji-point_down_tone2 {
+ background-position: 0 -700px;
+}
+.emoji-point_down_tone3 {
+ background-position: -20px -700px;
+}
+.emoji-point_down_tone4 {
+ background-position: -40px -700px;
+}
+.emoji-point_down_tone5 {
+ background-position: -60px -700px;
+}
+.emoji-point_left {
+ background-position: -80px -700px;
+}
+.emoji-point_left_tone1 {
+ background-position: -100px -700px;
+}
+.emoji-point_left_tone2 {
+ background-position: -120px -700px;
+}
+.emoji-point_left_tone3 {
+ background-position: -140px -700px;
+}
+.emoji-point_left_tone4 {
+ background-position: -160px -700px;
+}
+.emoji-point_left_tone5 {
+ background-position: -180px -700px;
+}
+.emoji-point_right {
+ background-position: -200px -700px;
+}
+.emoji-point_right_tone1 {
+ background-position: -220px -700px;
+}
+.emoji-point_right_tone2 {
+ background-position: -240px -700px;
+}
+.emoji-point_right_tone3 {
+ background-position: -260px -700px;
+}
+.emoji-point_right_tone4 {
+ background-position: -280px -700px;
+}
+.emoji-point_right_tone5 {
+ background-position: -300px -700px;
+}
+.emoji-point_up {
+ background-position: -320px -700px;
+}
+.emoji-point_up_2 {
+ background-position: -340px -700px;
+}
+.emoji-point_up_2_tone1 {
+ background-position: -360px -700px;
+}
+.emoji-point_up_2_tone2 {
+ background-position: -380px -700px;
+}
+.emoji-point_up_2_tone3 {
+ background-position: -400px -700px;
+}
+.emoji-point_up_2_tone4 {
+ background-position: -420px -700px;
+}
+.emoji-point_up_2_tone5 {
+ background-position: -440px -700px;
+}
+.emoji-point_up_tone1 {
+ background-position: -460px -700px;
+}
+.emoji-point_up_tone2 {
+ background-position: -480px -700px;
+}
+.emoji-point_up_tone3 {
+ background-position: -500px -700px;
+}
+.emoji-point_up_tone4 {
+ background-position: -520px -700px;
+}
+.emoji-point_up_tone5 {
+ background-position: -540px -700px;
+}
+.emoji-police_car {
+ background-position: -560px -700px;
+}
+.emoji-poodle {
+ background-position: -580px -700px;
+}
+.emoji-poop {
+ background-position: -600px -700px;
+}
+.emoji-popcorn {
+ background-position: -620px -700px;
+}
+.emoji-post_office {
+ background-position: -640px -700px;
+}
+.emoji-postal_horn {
+ background-position: -660px -700px;
+}
+.emoji-postbox {
+ background-position: -680px -700px;
+}
+.emoji-potable_water {
+ background-position: -700px -700px;
+}
+.emoji-potato {
+ background-position: -720px 0;
+}
+.emoji-pouch {
+ background-position: -720px -20px;
+}
+.emoji-poultry_leg {
+ background-position: -720px -40px;
+}
+.emoji-pound {
+ background-position: -720px -60px;
+}
+.emoji-pouting_cat {
+ background-position: -720px -80px;
+}
+.emoji-pray {
+ background-position: -720px -100px;
+}
+.emoji-pray_tone1 {
+ background-position: -720px -120px;
+}
+.emoji-pray_tone2 {
+ background-position: -720px -140px;
+}
+.emoji-pray_tone3 {
+ background-position: -720px -160px;
+}
+.emoji-pray_tone4 {
+ background-position: -720px -180px;
+}
+.emoji-pray_tone5 {
+ background-position: -720px -200px;
+}
+.emoji-prayer_beads {
+ background-position: -720px -220px;
+}
+.emoji-pregnant_woman {
+ background-position: -720px -240px;
+}
+.emoji-pregnant_woman_tone1 {
+ background-position: -720px -260px;
+}
+.emoji-pregnant_woman_tone2 {
+ background-position: -720px -280px;
+}
+.emoji-pregnant_woman_tone3 {
+ background-position: -720px -300px;
+}
+.emoji-pregnant_woman_tone4 {
+ background-position: -720px -320px;
+}
+.emoji-pregnant_woman_tone5 {
+ background-position: -720px -340px;
+}
+.emoji-prince {
+ background-position: -720px -360px;
+}
+.emoji-prince_tone1 {
+ background-position: -720px -380px;
+}
+.emoji-prince_tone2 {
+ background-position: -720px -400px;
+}
+.emoji-prince_tone3 {
+ background-position: -720px -420px;
+}
+.emoji-prince_tone4 {
+ background-position: -720px -440px;
+}
+.emoji-prince_tone5 {
+ background-position: -720px -460px;
+}
+.emoji-princess {
+ background-position: -720px -480px;
+}
+.emoji-princess_tone1 {
+ background-position: -720px -500px;
+}
+.emoji-princess_tone2 {
+ background-position: -720px -520px;
+}
+.emoji-princess_tone3 {
+ background-position: -720px -540px;
+}
+.emoji-princess_tone4 {
+ background-position: -720px -560px;
+}
+.emoji-princess_tone5 {
+ background-position: -720px -580px;
+}
+.emoji-printer {
+ background-position: -720px -600px;
+}
+.emoji-projector {
+ background-position: -720px -620px;
+}
+.emoji-punch {
+ background-position: -720px -640px;
+}
+.emoji-punch_tone1 {
+ background-position: -720px -660px;
+}
+.emoji-punch_tone2 {
+ background-position: -720px -680px;
+}
+.emoji-punch_tone3 {
+ background-position: -720px -700px;
+}
+.emoji-punch_tone4 {
+ background-position: 0 -720px;
+}
+.emoji-punch_tone5 {
+ background-position: -20px -720px;
+}
+.emoji-purple_heart {
+ background-position: -40px -720px;
+}
+.emoji-purse {
+ background-position: -60px -720px;
+}
+.emoji-pushpin {
+ background-position: -80px -720px;
+}
+.emoji-put_litter_in_its_place {
+ background-position: -100px -720px;
+}
+.emoji-question {
+ background-position: -120px -720px;
+}
+.emoji-rabbit {
+ background-position: -140px -720px;
+}
+.emoji-rabbit2 {
+ background-position: -160px -720px;
+}
+.emoji-race_car {
+ background-position: -180px -720px;
+}
+.emoji-racehorse {
+ background-position: -200px -720px;
+}
+.emoji-radio {
+ background-position: -220px -720px;
+}
+.emoji-radio_button {
+ background-position: -240px -720px;
+}
+.emoji-radioactive {
+ background-position: -260px -720px;
+}
+.emoji-rage {
+ background-position: -280px -720px;
+}
+.emoji-railway_car {
+ background-position: -300px -720px;
+}
+.emoji-railway_track {
+ background-position: -320px -720px;
+}
+.emoji-rainbow {
+ background-position: -340px -720px;
+}
+.emoji-raised_back_of_hand {
+ background-position: -360px -720px;
+}
+.emoji-raised_back_of_hand_tone1 {
+ background-position: -380px -720px;
+}
+.emoji-raised_back_of_hand_tone2 {
+ background-position: -400px -720px;
+}
+.emoji-raised_back_of_hand_tone3 {
+ background-position: -420px -720px;
+}
+.emoji-raised_back_of_hand_tone4 {
+ background-position: -440px -720px;
+}
+.emoji-raised_back_of_hand_tone5 {
+ background-position: -460px -720px;
+}
+.emoji-raised_hand {
+ background-position: -480px -720px;
+}
+.emoji-raised_hand_tone1 {
+ background-position: -500px -720px;
+}
+.emoji-raised_hand_tone2 {
+ background-position: -520px -720px;
+}
+.emoji-raised_hand_tone3 {
+ background-position: -540px -720px;
+}
+.emoji-raised_hand_tone4 {
+ background-position: -560px -720px;
+}
+.emoji-raised_hand_tone5 {
+ background-position: -580px -720px;
+}
+.emoji-raised_hands {
+ background-position: -600px -720px;
+}
+.emoji-raised_hands_tone1 {
+ background-position: -620px -720px;
+}
+.emoji-raised_hands_tone2 {
+ background-position: -640px -720px;
+}
+.emoji-raised_hands_tone3 {
+ background-position: -660px -720px;
+}
+.emoji-raised_hands_tone4 {
+ background-position: -680px -720px;
+}
+.emoji-raised_hands_tone5 {
+ background-position: -700px -720px;
+}
+.emoji-raising_hand {
+ background-position: -720px -720px;
+}
+.emoji-raising_hand_tone1 {
+ background-position: -740px 0;
+}
+.emoji-raising_hand_tone2 {
+ background-position: -740px -20px;
+}
+.emoji-raising_hand_tone3 {
+ background-position: -740px -40px;
+}
+.emoji-raising_hand_tone4 {
+ background-position: -740px -60px;
+}
+.emoji-raising_hand_tone5 {
+ background-position: -740px -80px;
+}
+.emoji-ram {
+ background-position: -740px -100px;
+}
+.emoji-ramen {
+ background-position: -740px -120px;
+}
+.emoji-rat {
+ background-position: -740px -140px;
+}
+.emoji-record_button {
+ background-position: -740px -160px;
+}
+.emoji-recycle {
+ background-position: -740px -180px;
+}
+.emoji-red_car {
+ background-position: -740px -200px;
+}
+.emoji-red_circle {
+ background-position: -740px -220px;
+}
+.emoji-registered {
+ background-position: -740px -240px;
+}
+.emoji-relaxed {
+ background-position: -740px -260px;
+}
+.emoji-relieved {
+ background-position: -740px -280px;
+}
+.emoji-reminder_ribbon {
+ background-position: -740px -300px;
+}
+.emoji-repeat {
+ background-position: -740px -320px;
+}
+.emoji-repeat_one {
+ background-position: -740px -340px;
+}
+.emoji-restroom {
+ background-position: -740px -360px;
+}
+.emoji-revolving_hearts {
+ background-position: -740px -380px;
+}
+.emoji-rewind {
+ background-position: -740px -400px;
+}
+.emoji-rhino {
+ background-position: -740px -420px;
+}
+.emoji-ribbon {
+ background-position: -740px -440px;
+}
+.emoji-rice {
+ background-position: -740px -460px;
+}
+.emoji-rice_ball {
+ background-position: -740px -480px;
+}
+.emoji-rice_cracker {
+ background-position: -740px -500px;
+}
+.emoji-rice_scene {
+ background-position: -740px -520px;
+}
+.emoji-right_facing_fist {
+ background-position: -740px -540px;
+}
+.emoji-right_facing_fist_tone1 {
+ background-position: -740px -560px;
+}
+.emoji-right_facing_fist_tone2 {
+ background-position: -740px -580px;
+}
+.emoji-right_facing_fist_tone3 {
+ background-position: -740px -600px;
+}
+.emoji-right_facing_fist_tone4 {
+ background-position: -740px -620px;
+}
+.emoji-right_facing_fist_tone5 {
+ background-position: -740px -640px;
+}
+.emoji-ring {
+ background-position: -740px -660px;
+}
+.emoji-robot {
+ background-position: -740px -680px;
+}
+.emoji-rocket {
+ background-position: -740px -700px;
+}
+.emoji-rofl {
+ background-position: -740px -720px;
+}
+.emoji-roller_coaster {
+ background-position: 0 -740px;
+}
+.emoji-rolling_eyes {
+ background-position: -20px -740px;
+}
+.emoji-rooster {
+ background-position: -40px -740px;
+}
+.emoji-rose {
+ background-position: -60px -740px;
+}
+.emoji-rosette {
+ background-position: -80px -740px;
+}
+.emoji-rotating_light {
+ background-position: -100px -740px;
+}
+.emoji-round_pushpin {
+ background-position: -120px -740px;
+}
+.emoji-rowboat {
+ background-position: -140px -740px;
+}
+.emoji-rowboat_tone1 {
+ background-position: -160px -740px;
+}
+.emoji-rowboat_tone2 {
+ background-position: -180px -740px;
+}
+.emoji-rowboat_tone3 {
+ background-position: -200px -740px;
+}
+.emoji-rowboat_tone4 {
+ background-position: -220px -740px;
+}
+.emoji-rowboat_tone5 {
+ background-position: -240px -740px;
+}
+.emoji-rugby_football {
+ background-position: -260px -740px;
+}
+.emoji-runner {
+ background-position: -280px -740px;
+}
+.emoji-runner_tone1 {
+ background-position: -300px -740px;
+}
+.emoji-runner_tone2 {
+ background-position: -320px -740px;
+}
+.emoji-runner_tone3 {
+ background-position: -340px -740px;
+}
+.emoji-runner_tone4 {
+ background-position: -360px -740px;
+}
+.emoji-runner_tone5 {
+ background-position: -380px -740px;
+}
+.emoji-running_shirt_with_sash {
+ background-position: -400px -740px;
+}
+.emoji-sa {
+ background-position: -420px -740px;
+}
+.emoji-sagittarius {
+ background-position: -440px -740px;
+}
+.emoji-sailboat {
+ background-position: -460px -740px;
+}
+.emoji-sake {
+ background-position: -480px -740px;
+}
+.emoji-salad {
+ background-position: -500px -740px;
+}
+.emoji-sandal {
+ background-position: -520px -740px;
+}
+.emoji-santa {
+ background-position: -540px -740px;
+}
+.emoji-santa_tone1 {
+ background-position: -560px -740px;
+}
+.emoji-santa_tone2 {
+ background-position: -580px -740px;
+}
+.emoji-santa_tone3 {
+ background-position: -600px -740px;
+}
+.emoji-santa_tone4 {
+ background-position: -620px -740px;
+}
+.emoji-santa_tone5 {
+ background-position: -640px -740px;
+}
+.emoji-satellite {
+ background-position: -660px -740px;
+}
+.emoji-satellite_orbital {
+ background-position: -680px -740px;
+}
+.emoji-saxophone {
+ background-position: -700px -740px;
+}
+.emoji-scales {
+ background-position: -720px -740px;
+}
+.emoji-school {
+ background-position: -740px -740px;
+}
+.emoji-school_satchel {
+ background-position: -760px 0;
+}
+.emoji-scissors {
+ background-position: -760px -20px;
+}
+.emoji-scooter {
+ background-position: -760px -40px;
+}
+.emoji-scorpion {
+ background-position: -760px -60px;
+}
+.emoji-scorpius {
+ background-position: -760px -80px;
+}
+.emoji-scream {
+ background-position: -760px -100px;
+}
+.emoji-scream_cat {
+ background-position: -760px -120px;
+}
+.emoji-scroll {
+ background-position: -760px -140px;
+}
+.emoji-seat {
+ background-position: -760px -160px;
+}
+.emoji-second_place {
+ background-position: -760px -180px;
+}
+.emoji-secret {
+ background-position: -760px -200px;
+}
+.emoji-see_no_evil {
+ background-position: -760px -220px;
+}
+.emoji-seedling {
+ background-position: -760px -240px;
+}
+.emoji-selfie {
+ background-position: -760px -260px;
+}
+.emoji-selfie_tone1 {
+ background-position: -760px -280px;
+}
+.emoji-selfie_tone2 {
+ background-position: -760px -300px;
+}
+.emoji-selfie_tone3 {
+ background-position: -760px -320px;
+}
+.emoji-selfie_tone4 {
+ background-position: -760px -340px;
+}
+.emoji-selfie_tone5 {
+ background-position: -760px -360px;
+}
+.emoji-seven {
+ background-position: -760px -380px;
+}
+.emoji-shallow_pan_of_food {
+ background-position: -760px -400px;
+}
+.emoji-shamrock {
+ background-position: -760px -420px;
+}
+.emoji-shark {
+ background-position: -760px -440px;
+}
+.emoji-shaved_ice {
+ background-position: -760px -460px;
+}
+.emoji-sheep {
+ background-position: -760px -480px;
+}
+.emoji-shell {
+ background-position: -760px -500px;
+}
+.emoji-shield {
+ background-position: -760px -520px;
+}
+.emoji-shinto_shrine {
+ background-position: -760px -540px;
+}
+.emoji-ship {
+ background-position: -760px -560px;
+}
+.emoji-shirt {
+ background-position: -760px -580px;
+}
+.emoji-shopping_bags {
+ background-position: -760px -600px;
+}
+.emoji-shopping_cart {
+ background-position: -760px -620px;
+}
+.emoji-shower {
+ background-position: -760px -640px;
+}
+.emoji-shrimp {
+ background-position: -760px -660px;
+}
+.emoji-shrug {
+ background-position: -760px -680px;
+}
+.emoji-shrug_tone1 {
+ background-position: -760px -700px;
+}
+.emoji-shrug_tone2 {
+ background-position: -760px -720px;
+}
+.emoji-shrug_tone3 {
+ background-position: -760px -740px;
+}
+.emoji-shrug_tone4 {
+ background-position: 0 -760px;
+}
+.emoji-shrug_tone5 {
+ background-position: -20px -760px;
+}
+.emoji-signal_strength {
+ background-position: -40px -760px;
+}
+.emoji-six {
+ background-position: -60px -760px;
+}
+.emoji-six_pointed_star {
+ background-position: -80px -760px;
+}
+.emoji-ski {
+ background-position: -100px -760px;
+}
+.emoji-skier {
+ background-position: -120px -760px;
+}
+.emoji-skull {
+ background-position: -140px -760px;
+}
+.emoji-skull_crossbones {
+ background-position: -160px -760px;
+}
+.emoji-sleeping {
+ background-position: -180px -760px;
+}
+.emoji-sleeping_accommodation {
+ background-position: -200px -760px;
+}
+.emoji-sleepy {
+ background-position: -220px -760px;
+}
+.emoji-slight_frown {
+ background-position: -240px -760px;
+}
+.emoji-slight_smile {
+ background-position: -260px -760px;
+}
+.emoji-slot_machine {
+ background-position: -280px -760px;
+}
+.emoji-small_blue_diamond {
+ background-position: -300px -760px;
+}
+.emoji-small_orange_diamond {
+ background-position: -320px -760px;
+}
+.emoji-small_red_triangle {
+ background-position: -340px -760px;
+}
+.emoji-small_red_triangle_down {
+ background-position: -360px -760px;
+}
+.emoji-smile {
+ background-position: -380px -760px;
+}
+.emoji-smile_cat {
+ background-position: -400px -760px;
+}
+.emoji-smiley {
+ background-position: -420px -760px;
+}
+.emoji-smiley_cat {
+ background-position: -440px -760px;
+}
+.emoji-smiling_imp {
+ background-position: -460px -760px;
+}
+.emoji-smirk {
+ background-position: -480px -760px;
+}
+.emoji-smirk_cat {
+ background-position: -500px -760px;
+}
+.emoji-smoking {
+ background-position: -520px -760px;
+}
+.emoji-snail {
+ background-position: -540px -760px;
+}
+.emoji-snake {
+ background-position: -560px -760px;
+}
+.emoji-sneezing_face {
+ background-position: -580px -760px;
+}
+.emoji-snowboarder {
+ background-position: -600px -760px;
+}
+.emoji-snowflake {
+ background-position: -620px -760px;
+}
+.emoji-snowman {
+ background-position: -640px -760px;
+}
+.emoji-snowman2 {
+ background-position: -660px -760px;
+}
+.emoji-sob {
+ background-position: -680px -760px;
+}
+.emoji-soccer {
+ background-position: -700px -760px;
+}
+.emoji-soon {
+ background-position: -720px -760px;
+}
+.emoji-sos {
+ background-position: -740px -760px;
+}
+.emoji-sound {
+ background-position: -760px -760px;
+}
+.emoji-space_invader {
+ background-position: -780px 0;
+}
+.emoji-spades {
+ background-position: -780px -20px;
+}
+.emoji-spaghetti {
+ background-position: -780px -40px;
+}
+.emoji-sparkle {
+ background-position: -780px -60px;
+}
+.emoji-sparkler {
+ background-position: -780px -80px;
+}
+.emoji-sparkles {
+ background-position: -780px -100px;
+}
+.emoji-sparkling_heart {
+ background-position: -780px -120px;
+}
+.emoji-speak_no_evil {
+ background-position: -780px -140px;
+}
+.emoji-speaker {
+ background-position: -780px -160px;
+}
+.emoji-speaking_head {
+ background-position: -780px -180px;
+}
+.emoji-speech_balloon {
+ background-position: -780px -200px;
+}
+.emoji-speech_left {
+ background-position: -780px -220px;
+}
+.emoji-speedboat {
+ background-position: -780px -240px;
+}
+.emoji-spider {
+ background-position: -780px -260px;
+}
+.emoji-spider_web {
+ background-position: -780px -280px;
+}
+.emoji-spoon {
+ background-position: -780px -300px;
+}
+.emoji-spy {
+ background-position: -780px -320px;
+}
+.emoji-spy_tone1 {
+ background-position: -780px -340px;
+}
+.emoji-spy_tone2 {
+ background-position: -780px -360px;
+}
+.emoji-spy_tone3 {
+ background-position: -780px -380px;
+}
+.emoji-spy_tone4 {
+ background-position: -780px -400px;
+}
+.emoji-spy_tone5 {
+ background-position: -780px -420px;
+}
+.emoji-squid {
+ background-position: -780px -440px;
+}
+.emoji-stadium {
+ background-position: -780px -460px;
+}
+.emoji-star {
+ background-position: -780px -480px;
+}
+.emoji-star2 {
+ background-position: -780px -500px;
+}
+.emoji-star_and_crescent {
+ background-position: -780px -520px;
+}
+.emoji-star_of_david {
+ background-position: -780px -540px;
+}
+.emoji-stars {
+ background-position: -780px -560px;
+}
+.emoji-station {
+ background-position: -780px -580px;
+}
+.emoji-statue_of_liberty {
+ background-position: -780px -600px;
+}
+.emoji-steam_locomotive {
+ background-position: -780px -620px;
+}
+.emoji-stew {
+ background-position: -780px -640px;
+}
+.emoji-stop_button {
+ background-position: -780px -660px;
+}
+.emoji-stopwatch {
+ background-position: -780px -680px;
+}
+.emoji-straight_ruler {
+ background-position: -780px -700px;
+}
+.emoji-strawberry {
+ background-position: -780px -720px;
+}
+.emoji-stuck_out_tongue {
+ background-position: -780px -740px;
+}
+.emoji-stuck_out_tongue_closed_eyes {
+ background-position: -780px -760px;
+}
+.emoji-stuck_out_tongue_winking_eye {
+ background-position: 0 -780px;
+}
+.emoji-stuffed_flatbread {
+ background-position: -20px -780px;
+}
+.emoji-sun_with_face {
+ background-position: -40px -780px;
+}
+.emoji-sunflower {
+ background-position: -60px -780px;
+}
+.emoji-sunglasses {
+ background-position: -80px -780px;
+}
+.emoji-sunny {
+ background-position: -100px -780px;
+}
+.emoji-sunrise {
+ background-position: -120px -780px;
+}
+.emoji-sunrise_over_mountains {
+ background-position: -140px -780px;
+}
+.emoji-surfer {
+ background-position: -160px -780px;
+}
+.emoji-surfer_tone1 {
+ background-position: -180px -780px;
+}
+.emoji-surfer_tone2 {
+ background-position: -200px -780px;
+}
+.emoji-surfer_tone3 {
+ background-position: -220px -780px;
+}
+.emoji-surfer_tone4 {
+ background-position: -240px -780px;
+}
+.emoji-surfer_tone5 {
+ background-position: -260px -780px;
+}
+.emoji-sushi {
+ background-position: -280px -780px;
+}
+.emoji-suspension_railway {
+ background-position: -300px -780px;
+}
+.emoji-sweat {
+ background-position: -320px -780px;
+}
+.emoji-sweat_drops {
+ background-position: -340px -780px;
+}
+.emoji-sweat_smile {
+ background-position: -360px -780px;
+}
+.emoji-sweet_potato {
+ background-position: -380px -780px;
+}
+.emoji-swimmer {
+ background-position: -400px -780px;
+}
+.emoji-swimmer_tone1 {
+ background-position: -420px -780px;
+}
+.emoji-swimmer_tone2 {
+ background-position: -440px -780px;
+}
+.emoji-swimmer_tone3 {
+ background-position: -460px -780px;
+}
+.emoji-swimmer_tone4 {
+ background-position: -480px -780px;
+}
+.emoji-swimmer_tone5 {
+ background-position: -500px -780px;
+}
+.emoji-symbols {
+ background-position: -520px -780px;
+}
+.emoji-synagogue {
+ background-position: -540px -780px;
+}
+.emoji-syringe {
+ background-position: -560px -780px;
+}
+.emoji-taco {
+ background-position: -580px -780px;
+}
+.emoji-tada {
+ background-position: -600px -780px;
+}
+.emoji-tanabata_tree {
+ background-position: -620px -780px;
+}
+.emoji-tangerine {
+ background-position: -640px -780px;
+}
+.emoji-taurus {
+ background-position: -660px -780px;
+}
+.emoji-taxi {
+ background-position: -680px -780px;
+}
+.emoji-tea {
+ background-position: -700px -780px;
+}
+.emoji-telephone {
+ background-position: -720px -780px;
+}
+.emoji-telephone_receiver {
+ background-position: -740px -780px;
+}
+.emoji-telescope {
+ background-position: -760px -780px;
+}
+.emoji-ten {
+ background-position: -780px -780px;
+}
+.emoji-tennis {
+ background-position: -800px 0;
+}
+.emoji-tent {
+ background-position: -800px -20px;
+}
+.emoji-thermometer {
+ background-position: -800px -40px;
+}
+.emoji-thermometer_face {
+ background-position: -800px -60px;
+}
+.emoji-thinking {
+ background-position: -800px -80px;
+}
+.emoji-third_place {
+ background-position: -800px -100px;
+}
+.emoji-thought_balloon {
+ background-position: -800px -120px;
+}
+.emoji-three {
+ background-position: -800px -140px;
+}
+.emoji-thumbsdown {
+ background-position: -800px -160px;
+}
+.emoji-thumbsdown_tone1 {
+ background-position: -800px -180px;
+}
+.emoji-thumbsdown_tone2 {
+ background-position: -800px -200px;
+}
+.emoji-thumbsdown_tone3 {
+ background-position: -800px -220px;
+}
+.emoji-thumbsdown_tone4 {
+ background-position: -800px -240px;
+}
+.emoji-thumbsdown_tone5 {
+ background-position: -800px -260px;
+}
+.emoji-thumbsup {
+ background-position: -800px -280px;
+}
+.emoji-thumbsup_tone1 {
+ background-position: -800px -300px;
+}
+.emoji-thumbsup_tone2 {
+ background-position: -800px -320px;
+}
+.emoji-thumbsup_tone3 {
+ background-position: -800px -340px;
+}
+.emoji-thumbsup_tone4 {
+ background-position: -800px -360px;
+}
+.emoji-thumbsup_tone5 {
+ background-position: -800px -380px;
+}
+.emoji-thunder_cloud_rain {
+ background-position: -800px -400px;
+}
+.emoji-ticket {
+ background-position: -800px -420px;
+}
+.emoji-tickets {
+ background-position: -800px -440px;
+}
+.emoji-tiger {
+ background-position: -800px -460px;
+}
+.emoji-tiger2 {
+ background-position: -800px -480px;
+}
+.emoji-timer {
+ background-position: -800px -500px;
+}
+.emoji-tired_face {
+ background-position: -800px -520px;
+}
+.emoji-tm {
+ background-position: -800px -540px;
+}
+.emoji-toilet {
+ background-position: -800px -560px;
+}
+.emoji-tokyo_tower {
+ background-position: -800px -580px;
+}
+.emoji-tomato {
+ background-position: -800px -600px;
+}
+.emoji-tone1 {
+ background-position: -800px -620px;
+}
+.emoji-tone2 {
+ background-position: -800px -640px;
+}
+.emoji-tone3 {
+ background-position: -800px -660px;
+}
+.emoji-tone4 {
+ background-position: -800px -680px;
+}
+.emoji-tone5 {
+ background-position: -800px -700px;
+}
+.emoji-tongue {
+ background-position: -800px -720px;
+}
+.emoji-tools {
+ background-position: -800px -740px;
+}
+.emoji-top {
+ background-position: -800px -760px;
+}
+.emoji-tophat {
+ background-position: -800px -780px;
+}
+.emoji-track_next {
+ background-position: 0 -800px;
+}
+.emoji-track_previous {
+ background-position: -20px -800px;
+}
+.emoji-trackball {
+ background-position: -40px -800px;
+}
+.emoji-tractor {
+ background-position: -60px -800px;
+}
+.emoji-traffic_light {
+ background-position: -80px -800px;
+}
+.emoji-train {
+ background-position: -100px -800px;
+}
+.emoji-train2 {
+ background-position: -120px -800px;
+}
+.emoji-tram {
+ background-position: -140px -800px;
+}
+.emoji-triangular_flag_on_post {
+ background-position: -160px -800px;
+}
+.emoji-triangular_ruler {
+ background-position: -180px -800px;
+}
+.emoji-trident {
+ background-position: -200px -800px;
+}
+.emoji-triumph {
+ background-position: -220px -800px;
+}
+.emoji-trolleybus {
+ background-position: -240px -800px;
+}
+.emoji-trophy {
+ background-position: -260px -800px;
+}
+.emoji-tropical_drink {
+ background-position: -280px -800px;
+}
+.emoji-tropical_fish {
+ background-position: -300px -800px;
+}
+.emoji-truck {
+ background-position: -320px -800px;
+}
+.emoji-trumpet {
+ background-position: -340px -800px;
+}
+.emoji-tulip {
+ background-position: -360px -800px;
+}
+.emoji-tumbler_glass {
+ background-position: -380px -800px;
+}
+.emoji-turkey {
+ background-position: -400px -800px;
+}
+.emoji-turtle {
+ background-position: -420px -800px;
+}
+.emoji-tv {
+ background-position: -440px -800px;
+}
+.emoji-twisted_rightwards_arrows {
+ background-position: -460px -800px;
+}
+.emoji-two {
+ background-position: -480px -800px;
+}
+.emoji-two_hearts {
+ background-position: -500px -800px;
+}
+.emoji-two_men_holding_hands {
+ background-position: -520px -800px;
+}
+.emoji-two_women_holding_hands {
+ background-position: -540px -800px;
+}
+.emoji-u5272 {
+ background-position: -560px -800px;
+}
+.emoji-u5408 {
+ background-position: -580px -800px;
+}
+.emoji-u55b6 {
+ background-position: -600px -800px;
+}
+.emoji-u6307 {
+ background-position: -620px -800px;
+}
+.emoji-u6708 {
+ background-position: -640px -800px;
+}
+.emoji-u6709 {
+ background-position: -660px -800px;
+}
+.emoji-u6e80 {
+ background-position: -680px -800px;
+}
+.emoji-u7121 {
+ background-position: -700px -800px;
+}
+.emoji-u7533 {
+ background-position: -720px -800px;
+}
+.emoji-u7981 {
+ background-position: -740px -800px;
+}
+.emoji-u7a7a {
+ background-position: -760px -800px;
+}
+.emoji-umbrella {
+ background-position: -780px -800px;
+}
+.emoji-umbrella2 {
+ background-position: -800px -800px;
+}
+.emoji-unamused {
+ background-position: -820px 0;
+}
+.emoji-underage {
+ background-position: -820px -20px;
+}
+.emoji-unicorn {
+ background-position: -820px -40px;
+}
+.emoji-unlock {
+ background-position: -820px -60px;
+}
+.emoji-up {
+ background-position: -820px -80px;
+}
+.emoji-upside_down {
+ background-position: -820px -100px;
+}
+.emoji-urn {
+ background-position: -820px -120px;
+}
+.emoji-v {
+ background-position: -820px -140px;
+}
+.emoji-v_tone1 {
+ background-position: -820px -160px;
+}
+.emoji-v_tone2 {
+ background-position: -820px -180px;
+}
+.emoji-v_tone3 {
+ background-position: -820px -200px;
+}
+.emoji-v_tone4 {
+ background-position: -820px -220px;
+}
+.emoji-v_tone5 {
+ background-position: -820px -240px;
+}
+.emoji-vertical_traffic_light {
+ background-position: -820px -260px;
+}
+.emoji-vhs {
+ background-position: -820px -280px;
+}
+.emoji-vibration_mode {
+ background-position: -820px -300px;
+}
+.emoji-video_camera {
+ background-position: -820px -320px;
+}
+.emoji-video_game {
+ background-position: -820px -340px;
+}
+.emoji-violin {
+ background-position: -820px -360px;
+}
+.emoji-virgo {
+ background-position: -820px -380px;
+}
+.emoji-volcano {
+ background-position: -820px -400px;
+}
+.emoji-volleyball {
+ background-position: -820px -420px;
+}
+.emoji-vs {
+ background-position: -820px -440px;
+}
+.emoji-vulcan {
+ background-position: -820px -460px;
+}
+.emoji-vulcan_tone1 {
+ background-position: -820px -480px;
+}
+.emoji-vulcan_tone2 {
+ background-position: -820px -500px;
+}
+.emoji-vulcan_tone3 {
+ background-position: -820px -520px;
+}
+.emoji-vulcan_tone4 {
+ background-position: -820px -540px;
+}
+.emoji-vulcan_tone5 {
+ background-position: -820px -560px;
+}
+.emoji-walking {
+ background-position: -820px -580px;
+}
+.emoji-walking_tone1 {
+ background-position: -820px -600px;
+}
+.emoji-walking_tone2 {
+ background-position: -820px -620px;
+}
+.emoji-walking_tone3 {
+ background-position: -820px -640px;
+}
+.emoji-walking_tone4 {
+ background-position: -820px -660px;
+}
+.emoji-walking_tone5 {
+ background-position: -820px -680px;
+}
+.emoji-waning_crescent_moon {
+ background-position: -820px -700px;
+}
+.emoji-waning_gibbous_moon {
+ background-position: -820px -720px;
+}
+.emoji-warning {
+ background-position: -820px -740px;
+}
+.emoji-wastebasket {
+ background-position: -820px -760px;
+}
+.emoji-watch {
+ background-position: -820px -780px;
+}
+.emoji-water_buffalo {
+ background-position: -820px -800px;
+}
+.emoji-water_polo {
+ background-position: 0 -820px;
+}
+.emoji-water_polo_tone1 {
+ background-position: -20px -820px;
+}
+.emoji-water_polo_tone2 {
+ background-position: -40px -820px;
+}
+.emoji-water_polo_tone3 {
+ background-position: -60px -820px;
+}
+.emoji-water_polo_tone4 {
+ background-position: -80px -820px;
+}
+.emoji-water_polo_tone5 {
+ background-position: -100px -820px;
+}
+.emoji-watermelon {
+ background-position: -120px -820px;
+}
+.emoji-wave {
+ background-position: -140px -820px;
+}
+.emoji-wave_tone1 {
+ background-position: -160px -820px;
+}
+.emoji-wave_tone2 {
+ background-position: -180px -820px;
+}
+.emoji-wave_tone3 {
+ background-position: -200px -820px;
+}
+.emoji-wave_tone4 {
+ background-position: -220px -820px;
+}
+.emoji-wave_tone5 {
+ background-position: -240px -820px;
+}
+.emoji-wavy_dash {
+ background-position: -260px -820px;
+}
+.emoji-waxing_crescent_moon {
+ background-position: -280px -820px;
+}
+.emoji-waxing_gibbous_moon {
+ background-position: -300px -820px;
+}
+.emoji-wc {
+ background-position: -320px -820px;
+}
+.emoji-weary {
+ background-position: -340px -820px;
+}
+.emoji-wedding {
+ background-position: -360px -820px;
+}
+.emoji-whale {
+ background-position: -380px -820px;
+}
+.emoji-whale2 {
+ background-position: -400px -820px;
+}
+.emoji-wheel_of_dharma {
+ background-position: -420px -820px;
+}
+.emoji-wheelchair {
+ background-position: -440px -820px;
+}
+.emoji-white_check_mark {
+ background-position: -460px -820px;
+}
+.emoji-white_circle {
+ background-position: -480px -820px;
+}
+.emoji-white_flower {
+ background-position: -500px -820px;
+}
+.emoji-white_large_square {
+ background-position: -520px -820px;
+}
+.emoji-white_medium_small_square {
+ background-position: -540px -820px;
+}
+.emoji-white_medium_square {
+ background-position: -560px -820px;
+}
+.emoji-white_small_square {
+ background-position: -580px -820px;
+}
+.emoji-white_square_button {
+ background-position: -600px -820px;
+}
+.emoji-white_sun_cloud {
+ background-position: -620px -820px;
+}
+.emoji-white_sun_rain_cloud {
+ background-position: -640px -820px;
+}
+.emoji-white_sun_small_cloud {
+ background-position: -660px -820px;
+}
+.emoji-wilted_rose {
+ background-position: -680px -820px;
+}
+.emoji-wind_blowing_face {
+ background-position: -700px -820px;
+}
+.emoji-wind_chime {
+ background-position: -720px -820px;
+}
+.emoji-wine_glass {
+ background-position: -740px -820px;
+}
+.emoji-wink {
+ background-position: -760px -820px;
+}
+.emoji-wolf {
+ background-position: -780px -820px;
+}
+.emoji-woman {
+ background-position: -800px -820px;
+}
+.emoji-woman_tone1 {
+ background-position: -820px -820px;
+}
+.emoji-woman_tone2 {
+ background-position: -840px 0;
+}
+.emoji-woman_tone3 {
+ background-position: -840px -20px;
+}
+.emoji-woman_tone4 {
+ background-position: -840px -40px;
+}
+.emoji-woman_tone5 {
+ background-position: -840px -60px;
+}
+.emoji-womans_clothes {
+ background-position: -840px -80px;
+}
+.emoji-womans_hat {
+ background-position: -840px -100px;
+}
+.emoji-womens {
+ background-position: -840px -120px;
+}
+.emoji-worried {
+ background-position: -840px -140px;
+}
+.emoji-wrench {
+ background-position: -840px -160px;
+}
+.emoji-wrestlers {
+ background-position: -840px -180px;
+}
+.emoji-wrestlers_tone1 {
+ background-position: -840px -200px;
+}
+.emoji-wrestlers_tone2 {
+ background-position: -840px -220px;
+}
+.emoji-wrestlers_tone3 {
+ background-position: -840px -240px;
+}
+.emoji-wrestlers_tone4 {
+ background-position: -840px -260px;
+}
+.emoji-wrestlers_tone5 {
+ background-position: -840px -280px;
+}
+.emoji-writing_hand {
+ background-position: -840px -300px;
+}
+.emoji-writing_hand_tone1 {
+ background-position: -840px -320px;
+}
+.emoji-writing_hand_tone2 {
+ background-position: -840px -340px;
+}
+.emoji-writing_hand_tone3 {
+ background-position: -840px -360px;
+}
+.emoji-writing_hand_tone4 {
+ background-position: -840px -380px;
+}
+.emoji-writing_hand_tone5 {
+ background-position: -840px -400px;
+}
+.emoji-x {
+ background-position: -840px -420px;
+}
+.emoji-yellow_heart {
+ background-position: -840px -440px;
+}
+.emoji-yen {
+ background-position: -840px -460px;
+}
+.emoji-yin_yang {
+ background-position: -840px -480px;
+}
+.emoji-yum {
+ background-position: -840px -500px;
+}
+.emoji-zap {
+ background-position: -840px -520px;
+}
+.emoji-zero {
+ background-position: -840px -540px;
+}
+.emoji-zipper_mouth {
+ background-position: -840px -560px;
+}
+.emoji-100 {
+ background-position: -840px -580px;
+}
+
+.emoji-icon {
+ background-image: image-url('emoji.png');
+ background-repeat: no-repeat;
+ color: transparent;
+ text-indent: -99em;
+ height: 20px;
+ width: 20px;
+
+ @media only screen and (-webkit-min-device-pixel-ratio: 2),
+ only screen and (min--moz-device-pixel-ratio: 2),
+ only screen and (-o-min-device-pixel-ratio: 2/1),
+ only screen and (min-device-pixel-ratio: 2),
+ only screen and (min-resolution: 192dpi),
+ only screen and (min-resolution: 2dppx) {
+ background-image: image-url('emoji@2x.png');
+ background-size: 860px 840px;
+ }
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 2fccfa4011c..7c28024001f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,64 +1,64 @@
-@import "framework/variables";
-@import "framework/mixins";
-@import 'framework/tw_bootstrap_variables';
-@import 'framework/tw_bootstrap';
-@import "framework/layout";
+@import 'framework/variables';
+@import 'framework/mixins';
+@import '../../../node_modules/bootstrap/scss/bootstrap';
+@import 'bootstrap_migration';
+@import 'framework/layout';
-@import "framework/animations";
-@import "framework/vue_transitions";
-@import "framework/avatar";
-@import "framework/asciidoctor";
-@import "framework/banner";
-@import "framework/blocks";
-@import "framework/buttons";
-@import "framework/badges";
-@import "framework/calendar";
-@import "framework/callout";
-@import "framework/common";
-@import "framework/dropdowns";
-@import "framework/files";
-@import "framework/filters";
-@import "framework/flash";
-@import "framework/forms";
-@import "framework/gfm";
-@import "framework/gitlab_theme";
-@import "framework/header";
-@import "framework/highlight";
-@import "framework/issue_box";
-@import "framework/jquery";
-@import "framework/lists";
-@import "framework/logo";
-@import "framework/markdown_area";
-@import "framework/media_object";
-@import "framework/mobile";
-@import "framework/modal";
-@import "framework/pagination";
-@import "framework/panels";
-@import "framework/popup";
-@import "framework/secondary_navigation_elements";
-@import "framework/selects";
-@import "framework/sidebar";
-@import "framework/contextual_sidebar";
-@import "framework/tables";
-@import "framework/notes";
-@import "framework/tabs";
-@import "framework/timeline";
-@import "framework/tooltips";
-@import "framework/toggle";
-@import "framework/typography";
-@import "framework/zen";
-@import "framework/blank";
-@import "framework/wells";
-@import "framework/page_header";
-@import "framework/awards";
-@import "framework/images";
-@import "framework/broadcast_messages";
-@import "framework/emojis";
-@import "framework/emoji_sprites";
-@import "framework/icons";
-@import "framework/snippets";
-@import "framework/memory_graph";
-@import "framework/responsive_tables";
-@import "framework/stacked_progress_bar";
-@import "framework/ci_variable_list";
-@import "framework/feature_highlight";
+@import 'framework/animations';
+@import 'framework/vue_transitions';
+@import 'framework/avatar';
+@import 'framework/asciidoctor';
+@import 'framework/banner';
+@import 'framework/blocks';
+@import 'framework/buttons';
+@import 'framework/badges';
+@import 'framework/calendar';
+@import 'framework/callout';
+@import 'framework/common';
+@import 'framework/dropdowns';
+@import 'framework/files';
+@import 'framework/filters';
+@import 'framework/flash';
+@import 'framework/forms';
+@import 'framework/gfm';
+@import 'framework/gitlab_theme';
+@import 'framework/header';
+@import 'framework/highlight';
+@import 'framework/issue_box';
+@import 'framework/jquery';
+@import 'framework/lists';
+@import 'framework/logo';
+@import 'framework/markdown_area';
+@import 'framework/media_object';
+@import 'framework/mobile';
+@import 'framework/modal';
+@import 'framework/pagination';
+@import 'framework/panels';
+@import 'framework/popup';
+@import 'framework/secondary_navigation_elements';
+@import 'framework/selects';
+@import 'framework/sidebar';
+@import 'framework/contextual_sidebar';
+@import 'framework/tables';
+@import 'framework/notes';
+@import 'framework/tabs';
+@import 'framework/timeline';
+@import 'framework/tooltips';
+@import 'framework/toggle';
+@import 'framework/typography';
+@import 'framework/zen';
+@import 'framework/blank';
+@import 'framework/wells';
+@import 'framework/page_header';
+@import 'framework/awards';
+@import 'framework/images';
+@import 'framework/broadcast_messages';
+@import 'framework/emojis';
+@import 'framework/icons';
+@import 'framework/snippets';
+@import 'framework/memory_graph';
+@import 'framework/responsive_tables';
+@import 'framework/stacked_progress_bar';
+@import 'framework/ci_variable_list';
+@import 'framework/feature_highlight';
+@import 'framework/terms';
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 077d0424093..c1ec11e434a 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -31,7 +31,7 @@
.avatar {
@extend .avatar-circle;
- @include transition-property(none);
+ transition-property: none;
width: 40px;
height: 40px;
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
index 6bbe32df772..57df9b969c3 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -1,4 +1,4 @@
-.badge {
+.badge.badge-pill {
font-weight: $gl-font-weight-normal;
background-color: $badge-bg;
color: $badge-color;
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
index 02f3896d591..71bbab2065d 100644
--- a/app/assets/stylesheets/framework/banner.scss
+++ b/app/assets/stylesheets/framework/banner.scss
@@ -23,7 +23,7 @@
border-bottom: 1px solid $border-color;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
justify-content: center;
flex-direction: column;
align-items: center;
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index 9982a5779af..91dbb2a6365 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -37,7 +37,7 @@
flex: 0 0 100%;
margin-bottom: 15px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
flex: 0 0 49%;
&:nth-child(odd) {
@@ -67,7 +67,7 @@
border: 1px solid $border-color;
border-radius: $border-radius-default;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: flex;
align-items: center;
padding: 50px 30px;
@@ -89,12 +89,12 @@
}
.blank-state-body {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
margin-top: 20px;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 20px;
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index c5c7afe25be..c5be27f2d29 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -46,7 +46,7 @@
}
&.middle-block {
- margin-top: 0;
+ margin-top: $gl-padding-24;
margin-bottom: 0;
}
@@ -61,7 +61,7 @@
}
&.footer-block {
- margin-top: 0;
+ margin-top: $gl-padding-24;
border-bottom: 0;
margin-bottom: -$gl-padding;
}
@@ -151,7 +151,7 @@
display: inline-block;
margin-left: 5px;
font-size: 18px;
- color: $gray;
+ color: color("gray");
}
p {
@@ -195,7 +195,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
.avatar {
@@ -315,7 +315,7 @@
}
}
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
flex-direction: column;
.inner-content {
@@ -342,7 +342,7 @@
.btn {
margin: $btn-side-margin 5px;
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
}
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index 9b54fb94cdc..d3e7d751e63 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -19,3 +19,9 @@
@extend .broadcast-message;
margin-bottom: 20px;
}
+
+.toggle-colors {
+ input {
+ min-height: 34px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index f4f5926e198..0115f542c88 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -106,10 +106,6 @@
@include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light);
}
-@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color);
-}
-
@mixin btn-white {
@include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color);
}
@@ -183,10 +179,6 @@
}
}
- &.btn-gray {
- @include btn-gray;
- }
-
&.btn-info,
&.btn-primary,
&.btn-register {
@@ -312,7 +304,8 @@
padding: 0 5px;
}
-.input-group-btn {
+.input-group-prepend,
+.input-group-append {
.btn {
@include btn-middle;
@@ -401,7 +394,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.btn-wide-on-xs {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index c165ec0b94b..0b9dff64b0b 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -4,7 +4,7 @@
border-top: 0;
direction: rtl;
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+ @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
overflow-x: auto;
}
}
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
index 5fe835dd8f9..7207e5119ce 100644
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ b/app/assets/stylesheets/framework/ci_variable_list.scss
@@ -10,14 +10,14 @@
display: flex;
align-items: flex-start;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
align-items: flex-end;
}
&:not(:last-child) {
margin-bottom: $gl-btn-padding;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-bottom: 3 * $gl-btn-padding;
}
}
@@ -26,7 +26,7 @@
.ci-variable-body-item:last-child {
margin-right: $ci-variable-remove-button-width;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-right: 0;
}
}
@@ -35,7 +35,7 @@
display: none;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.ci-variable-row-body {
margin-right: $ci-variable-remove-button-width;
}
@@ -48,7 +48,7 @@
align-items: flex-start;
width: 100%;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
}
}
@@ -59,7 +59,7 @@
&:not(:last-child) {
margin-right: $gl-btn-padding;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-right: 0;
margin-bottom: $gl-btn-padding;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 0db1b78ac9f..a8d13249ecb 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -70,7 +70,7 @@ pre {
padding: 0;
}
- &.well-pre {
+ &.card.card-body-pre {
border: 1px solid $well-pre-bg;
background: $gray-light;
border-radius: 0;
@@ -218,7 +218,7 @@ li.note {
}
.git_error_tips {
- @extend .col-md-6;
+ @extend .col-lg-6;
text-align: left;
margin-top: 40px;
@@ -308,7 +308,7 @@ img.emoji {
.btn-sign-in {
text-shadow: none;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-top: 8px;
}
}
@@ -327,7 +327,7 @@ img.emoji {
}
}
-.well {
+.card.card-body {
margin-bottom: $gl-padding;
hr {
@@ -336,7 +336,7 @@ img.emoji {
}
.search_box {
- @extend .well;
+ @extend .card.card-body;
text-align: center;
}
@@ -452,6 +452,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
+.prepend-top-2 { margin-top: 2px; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
@@ -474,6 +475,7 @@ img.emoji {
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
+.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index e2d97d0298f..1a415e1b852 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -1,11 +1,11 @@
.page-with-contextual-sidebar {
transition: padding-left $sidebar-transition-duration;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
padding-left: $contextual-sidebar-collapsed-width;
}
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
padding-left: $contextual-sidebar-width;
}
@@ -16,7 +16,7 @@
}
.page-with-icon-sidebar {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: $contextual-sidebar-collapsed-width;
}
}
@@ -75,7 +75,7 @@
transform: translate3d(0, 0, 0);
&:not(.sidebar-collapsed-desktop) {
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
+ @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
@@ -88,7 +88,7 @@
overflow-x: hidden;
}
- .badge:not(.fly-out-badge),
+ .badge.badge-pill:not(.fly-out-badge),
.sidebar-context-title,
.nav-item-name {
display: none;
@@ -142,7 +142,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
left: (-$contextual-sidebar-width);
}
@@ -166,7 +166,7 @@
width: 100%;
overflow: auto;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
overflow: hidden;
}
}
@@ -207,7 +207,7 @@
> li {
> a {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-right: 2px;
}
@@ -222,7 +222,7 @@
}
.sidebar-sub-level-items {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
position: fixed;
top: 0;
left: 0;
@@ -277,7 +277,7 @@
}
}
- .badge {
+ .badge.badge-pill {
background-color: $inactive-badge-background;
color: $gl-text-color-secondary;
}
@@ -291,7 +291,7 @@
padding-left: 11px;
}
- .badge {
+ .badge.badge-pill {
font-weight: $gl-font-weight-bold;
}
@@ -339,7 +339,7 @@
}
.toggle-sidebar-button {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: none;
}
}
@@ -420,7 +420,7 @@
color: $gl-text-color-secondary;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: flex;
align-items: center;
@@ -429,7 +429,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
+ .breadcrumbs-links {
padding-left: $gl-padding;
border-left: 1px solid $gl-text-color-quaternary;
@@ -437,7 +437,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.close-nav-button {
display: flex;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cc5fac6816d..1570b1f2eaa 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -24,7 +24,7 @@
display: none;
}
-.open {
+.show.dropdown {
.dropdown-menu,
.dropdown-menu-nav {
@include set-visible;
@@ -32,7 +32,7 @@
max-height: $dropdown-max-height;
overflow-y: auto;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
}
@@ -43,7 +43,7 @@
border-color: $gray-darkest;
}
- [data-toggle="dropdown"] {
+ [data-toggle='dropdown'] {
outline: 0;
}
}
@@ -172,7 +172,11 @@
color: $brand-danger;
}
- &:hover,
+ &.disable-hover {
+ text-decoration: none;
+ }
+
+ &:not(.disable-hover):hover,
&:active,
&:focus,
&.is-focused {
@@ -180,7 +184,7 @@
text-decoration: none;
- .badge {
+ .badge.badge-pill {
background-color: darken($dropdown-link-hover-bg, 5%);
}
}
@@ -206,11 +210,10 @@
.dropdown-menu,
.dropdown-menu-nav {
- @include set-invisible;
+ display: none;
position: absolute;
width: auto;
top: 100%;
- left: 0;
z-index: 300;
min-width: 240px;
max-width: 500px;
@@ -324,7 +327,7 @@
color: $gl-text-color-secondary;
}
- .badge + span:not(.badge) {
+ .badge.badge-pill + span:not(.badge.badge-pill) {
// Expects up to 3 digits on the badge
margin-right: 40px;
}
@@ -388,7 +391,7 @@
transform: translateY(0);
}
-.comment-type-dropdown.open .dropdown-menu {
+.comment-type-dropdown.show .dropdown-menu {
display: block;
}
@@ -469,16 +472,11 @@
.dropdown-select {
width: $dropdown-width;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
width: 100%;
}
}
-.dropdown-menu-align-right {
- left: auto;
- right: 0;
-}
-
.dropdown-menu-selectable {
li {
a,
@@ -508,17 +506,16 @@
}
&.is-indeterminate::before {
- content: "\f068";
+ content: '\f068';
}
&.is-active::before {
- content: "\f00c";
+ content: '\f00c';
}
}
}
}
-
.dropdown-title {
position: relative;
padding: 2px 25px 10px;
@@ -724,7 +721,6 @@
}
}
-
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
@@ -781,7 +777,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.navbar-gitlab {
li.header-projects,
li.header-more,
@@ -836,7 +832,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
width: 70%;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-direction: column;
width: 100%;
height: auto;
@@ -854,9 +850,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-frequent-container,
- .projects-list-search-container, {
+ .projects-list-search-container {
padding: 8px 0;
overflow-y: auto;
+
+ li.section-empty.section-failure {
+ color: $callout-danger-color;
+ }
}
.section-header,
@@ -867,13 +867,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
font-size: $gl-font-size;
}
- .projects-list-frequent-container,
- .projects-list-search-container {
- li.section-empty.section-failure {
- color: $callout-danger-color;
- }
- }
-
.search-input-container {
position: relative;
padding: 4px $gl-padding;
@@ -895,7 +888,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
height: 284px;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.projects-list-frequent-container {
width: auto;
height: auto;
@@ -905,8 +898,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
.projects-list-item-container {
- .project-item-avatar-container
- .project-item-metadata-container {
+ .project-item-avatar-container .project-item-metadata-container {
float: left;
}
@@ -937,7 +929,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
color: $gl-text-color-secondary;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.project-item-metadata-container {
float: none;
}
diff --git a/app/assets/stylesheets/framework/emoji_sprites.scss b/app/assets/stylesheets/framework/emoji_sprites.scss
deleted file mode 100644
index 0174e17b660..00000000000
--- a/app/assets/stylesheets/framework/emoji_sprites.scss
+++ /dev/null
@@ -1,1813 +0,0 @@
-.emoji-zzz { background-position: 0 0; }
-.emoji-1234 { background-position: -20px 0; }
-.emoji-1F627 { background-position: 0 -20px; }
-.emoji-8ball { background-position: -20px -20px; }
-.emoji-a { background-position: -40px 0; }
-.emoji-ab { background-position: -40px -20px; }
-.emoji-abc { background-position: 0 -40px; }
-.emoji-abcd { background-position: -20px -40px; }
-.emoji-accept { background-position: -40px -40px; }
-.emoji-aerial_tramway { background-position: -60px 0; }
-.emoji-airplane { background-position: -60px -20px; }
-.emoji-airplane_arriving { background-position: -60px -40px; }
-.emoji-airplane_departure { background-position: 0 -60px; }
-.emoji-airplane_small { background-position: -20px -60px; }
-.emoji-alarm_clock { background-position: -40px -60px; }
-.emoji-alembic { background-position: -60px -60px; }
-.emoji-alien { background-position: -80px 0; }
-.emoji-ambulance { background-position: -80px -20px; }
-.emoji-amphora { background-position: -80px -40px; }
-.emoji-anchor { background-position: -80px -60px; }
-.emoji-angel { background-position: 0 -80px; }
-.emoji-angel_tone1 { background-position: -20px -80px; }
-.emoji-angel_tone2 { background-position: -40px -80px; }
-.emoji-angel_tone3 { background-position: -60px -80px; }
-.emoji-angel_tone4 { background-position: -80px -80px; }
-.emoji-angel_tone5 { background-position: -100px 0; }
-.emoji-anger { background-position: -100px -20px; }
-.emoji-anger_right { background-position: -100px -40px; }
-.emoji-angry { background-position: -100px -60px; }
-.emoji-ant { background-position: -100px -80px; }
-.emoji-apple { background-position: 0 -100px; }
-.emoji-aquarius { background-position: -20px -100px; }
-.emoji-aries { background-position: -40px -100px; }
-.emoji-arrow_backward { background-position: -60px -100px; }
-.emoji-arrow_double_down { background-position: -80px -100px; }
-.emoji-arrow_double_up { background-position: -100px -100px; }
-.emoji-arrow_down { background-position: -120px 0; }
-.emoji-arrow_down_small { background-position: -120px -20px; }
-.emoji-arrow_forward { background-position: -120px -40px; }
-.emoji-arrow_heading_down { background-position: -120px -60px; }
-.emoji-arrow_heading_up { background-position: -120px -80px; }
-.emoji-arrow_left { background-position: -120px -100px; }
-.emoji-arrow_lower_left { background-position: 0 -120px; }
-.emoji-arrow_lower_right { background-position: -20px -120px; }
-.emoji-arrow_right { background-position: -40px -120px; }
-.emoji-arrow_right_hook { background-position: -60px -120px; }
-.emoji-arrow_up { background-position: -80px -120px; }
-.emoji-arrow_up_down { background-position: -100px -120px; }
-.emoji-arrow_up_small { background-position: -120px -120px; }
-.emoji-arrow_upper_left { background-position: -140px 0; }
-.emoji-arrow_upper_right { background-position: -140px -20px; }
-.emoji-arrows_clockwise { background-position: -140px -40px; }
-.emoji-arrows_counterclockwise { background-position: -140px -60px; }
-.emoji-art { background-position: -140px -80px; }
-.emoji-articulated_lorry { background-position: -140px -100px; }
-.emoji-asterisk { background-position: -140px -120px; }
-.emoji-astonished { background-position: 0 -140px; }
-.emoji-athletic_shoe { background-position: -20px -140px; }
-.emoji-atm { background-position: -40px -140px; }
-.emoji-atom { background-position: -60px -140px; }
-.emoji-avocado { background-position: -80px -140px; }
-.emoji-b { background-position: -100px -140px; }
-.emoji-baby { background-position: -120px -140px; }
-.emoji-baby_bottle { background-position: -140px -140px; }
-.emoji-baby_chick { background-position: -160px 0; }
-.emoji-baby_symbol { background-position: -160px -20px; }
-.emoji-baby_tone1 { background-position: -160px -40px; }
-.emoji-baby_tone2 { background-position: -160px -60px; }
-.emoji-baby_tone3 { background-position: -160px -80px; }
-.emoji-baby_tone4 { background-position: -160px -100px; }
-.emoji-baby_tone5 { background-position: -160px -120px; }
-.emoji-back { background-position: -160px -140px; }
-.emoji-bacon { background-position: 0 -160px; }
-.emoji-badminton { background-position: -20px -160px; }
-.emoji-baggage_claim { background-position: -40px -160px; }
-.emoji-balloon { background-position: -60px -160px; }
-.emoji-ballot_box { background-position: -80px -160px; }
-.emoji-ballot_box_with_check { background-position: -100px -160px; }
-.emoji-bamboo { background-position: -120px -160px; }
-.emoji-banana { background-position: -140px -160px; }
-.emoji-bangbang { background-position: -160px -160px; }
-.emoji-bank { background-position: -180px 0; }
-.emoji-bar_chart { background-position: -180px -20px; }
-.emoji-barber { background-position: -180px -40px; }
-.emoji-baseball { background-position: -180px -60px; }
-.emoji-basketball { background-position: -180px -80px; }
-.emoji-basketball_player { background-position: -180px -100px; }
-.emoji-basketball_player_tone1 { background-position: -180px -120px; }
-.emoji-basketball_player_tone2 { background-position: -180px -140px; }
-.emoji-basketball_player_tone3 { background-position: -180px -160px; }
-.emoji-basketball_player_tone4 { background-position: 0 -180px; }
-.emoji-basketball_player_tone5 { background-position: -20px -180px; }
-.emoji-bat { background-position: -40px -180px; }
-.emoji-bath { background-position: -60px -180px; }
-.emoji-bath_tone1 { background-position: -80px -180px; }
-.emoji-bath_tone2 { background-position: -100px -180px; }
-.emoji-bath_tone3 { background-position: -120px -180px; }
-.emoji-bath_tone4 { background-position: -140px -180px; }
-.emoji-bath_tone5 { background-position: -160px -180px; }
-.emoji-bathtub { background-position: -180px -180px; }
-.emoji-battery { background-position: -200px 0; }
-.emoji-beach { background-position: -200px -20px; }
-.emoji-beach_umbrella { background-position: -200px -40px; }
-.emoji-bear { background-position: -200px -60px; }
-.emoji-bed { background-position: -200px -80px; }
-.emoji-bee { background-position: -200px -100px; }
-.emoji-beer { background-position: -200px -120px; }
-.emoji-beers { background-position: -200px -140px; }
-.emoji-beetle { background-position: -200px -160px; }
-.emoji-beginner { background-position: -200px -180px; }
-.emoji-bell { background-position: 0 -200px; }
-.emoji-bellhop { background-position: -20px -200px; }
-.emoji-bento { background-position: -40px -200px; }
-.emoji-bicyclist { background-position: -60px -200px; }
-.emoji-bicyclist_tone1 { background-position: -80px -200px; }
-.emoji-bicyclist_tone2 { background-position: -100px -200px; }
-.emoji-bicyclist_tone3 { background-position: -120px -200px; }
-.emoji-bicyclist_tone4 { background-position: -140px -200px; }
-.emoji-bicyclist_tone5 { background-position: -160px -200px; }
-.emoji-bike { background-position: -180px -200px; }
-.emoji-bikini { background-position: -200px -200px; }
-.emoji-biohazard { background-position: -220px 0; }
-.emoji-bird { background-position: -220px -20px; }
-.emoji-birthday { background-position: -220px -40px; }
-.emoji-black_circle { background-position: -220px -60px; }
-.emoji-black_heart { background-position: -220px -80px; }
-.emoji-black_joker { background-position: -220px -100px; }
-.emoji-black_large_square { background-position: -220px -120px; }
-.emoji-black_medium_small_square { background-position: -220px -140px; }
-.emoji-black_medium_square { background-position: -220px -160px; }
-.emoji-black_nib { background-position: -220px -180px; }
-.emoji-black_small_square { background-position: -220px -200px; }
-.emoji-black_square_button { background-position: 0 -220px; }
-.emoji-blossom { background-position: -20px -220px; }
-.emoji-blowfish { background-position: -40px -220px; }
-.emoji-blue_book { background-position: -60px -220px; }
-.emoji-blue_car { background-position: -80px -220px; }
-.emoji-blue_heart { background-position: -100px -220px; }
-.emoji-blush { background-position: -120px -220px; }
-.emoji-boar { background-position: -140px -220px; }
-.emoji-bomb { background-position: -160px -220px; }
-.emoji-book { background-position: -180px -220px; }
-.emoji-bookmark { background-position: -200px -220px; }
-.emoji-bookmark_tabs { background-position: -220px -220px; }
-.emoji-books { background-position: -240px 0; }
-.emoji-boom { background-position: -240px -20px; }
-.emoji-boot { background-position: -240px -40px; }
-.emoji-bouquet { background-position: -240px -60px; }
-.emoji-bow { background-position: -240px -80px; }
-.emoji-bow_and_arrow { background-position: -240px -100px; }
-.emoji-bow_tone1 { background-position: -240px -120px; }
-.emoji-bow_tone2 { background-position: -240px -140px; }
-.emoji-bow_tone3 { background-position: -240px -160px; }
-.emoji-bow_tone4 { background-position: -240px -180px; }
-.emoji-bow_tone5 { background-position: -240px -200px; }
-.emoji-bowling { background-position: -240px -220px; }
-.emoji-boxing_glove { background-position: 0 -240px; }
-.emoji-boy { background-position: -20px -240px; }
-.emoji-boy_tone1 { background-position: -40px -240px; }
-.emoji-boy_tone2 { background-position: -60px -240px; }
-.emoji-boy_tone3 { background-position: -80px -240px; }
-.emoji-boy_tone4 { background-position: -100px -240px; }
-.emoji-boy_tone5 { background-position: -120px -240px; }
-.emoji-bread { background-position: -140px -240px; }
-.emoji-bride_with_veil { background-position: -160px -240px; }
-.emoji-bride_with_veil_tone1 { background-position: -180px -240px; }
-.emoji-bride_with_veil_tone2 { background-position: -200px -240px; }
-.emoji-bride_with_veil_tone3 { background-position: -220px -240px; }
-.emoji-bride_with_veil_tone4 { background-position: -240px -240px; }
-.emoji-bride_with_veil_tone5 { background-position: -260px 0; }
-.emoji-bridge_at_night { background-position: -260px -20px; }
-.emoji-briefcase { background-position: -260px -40px; }
-.emoji-broken_heart { background-position: -260px -60px; }
-.emoji-bug { background-position: -260px -80px; }
-.emoji-bulb { background-position: -260px -100px; }
-.emoji-bullettrain_front { background-position: -260px -120px; }
-.emoji-bullettrain_side { background-position: -260px -140px; }
-.emoji-burrito { background-position: -260px -160px; }
-.emoji-bus { background-position: -260px -180px; }
-.emoji-busstop { background-position: -260px -200px; }
-.emoji-bust_in_silhouette { background-position: -260px -220px; }
-.emoji-busts_in_silhouette { background-position: -260px -240px; }
-.emoji-butterfly { background-position: 0 -260px; }
-.emoji-cactus { background-position: -20px -260px; }
-.emoji-cake { background-position: -40px -260px; }
-.emoji-calendar { background-position: -60px -260px; }
-.emoji-calendar_spiral { background-position: -80px -260px; }
-.emoji-call_me { background-position: -100px -260px; }
-.emoji-call_me_tone1 { background-position: -120px -260px; }
-.emoji-call_me_tone2 { background-position: -140px -260px; }
-.emoji-call_me_tone3 { background-position: -160px -260px; }
-.emoji-call_me_tone4 { background-position: -180px -260px; }
-.emoji-call_me_tone5 { background-position: -200px -260px; }
-.emoji-calling { background-position: -220px -260px; }
-.emoji-camel { background-position: -240px -260px; }
-.emoji-camera { background-position: -260px -260px; }
-.emoji-camera_with_flash { background-position: -280px 0; }
-.emoji-camping { background-position: -280px -20px; }
-.emoji-cancer { background-position: -280px -40px; }
-.emoji-candle { background-position: -280px -60px; }
-.emoji-candy { background-position: -280px -80px; }
-.emoji-canoe { background-position: -280px -100px; }
-.emoji-capital_abcd { background-position: -280px -120px; }
-.emoji-capricorn { background-position: -280px -140px; }
-.emoji-card_box { background-position: -280px -160px; }
-.emoji-card_index { background-position: -280px -180px; }
-.emoji-carousel_horse { background-position: -280px -200px; }
-.emoji-carrot { background-position: -280px -220px; }
-.emoji-cartwheel { background-position: -280px -240px; }
-.emoji-cartwheel_tone1 { background-position: -280px -260px; }
-.emoji-cartwheel_tone2 { background-position: 0 -280px; }
-.emoji-cartwheel_tone3 { background-position: -20px -280px; }
-.emoji-cartwheel_tone4 { background-position: -40px -280px; }
-.emoji-cartwheel_tone5 { background-position: -60px -280px; }
-.emoji-cat { background-position: -80px -280px; }
-.emoji-cat2 { background-position: -100px -280px; }
-.emoji-cd { background-position: -120px -280px; }
-.emoji-chains { background-position: -140px -280px; }
-.emoji-champagne { background-position: -160px -280px; }
-.emoji-champagne_glass { background-position: -180px -280px; }
-.emoji-chart { background-position: -200px -280px; }
-.emoji-chart_with_downwards_trend { background-position: -220px -280px; }
-.emoji-chart_with_upwards_trend { background-position: -240px -280px; }
-.emoji-checkered_flag { background-position: -260px -280px; }
-.emoji-cheese { background-position: -280px -280px; }
-.emoji-cherries { background-position: -300px 0; }
-.emoji-cherry_blossom { background-position: -300px -20px; }
-.emoji-chestnut { background-position: -300px -40px; }
-.emoji-chicken { background-position: -300px -60px; }
-.emoji-children_crossing { background-position: -300px -80px; }
-.emoji-chipmunk { background-position: -300px -100px; }
-.emoji-chocolate_bar { background-position: -300px -120px; }
-.emoji-christmas_tree { background-position: -300px -140px; }
-.emoji-church { background-position: -300px -160px; }
-.emoji-cinema { background-position: -300px -180px; }
-.emoji-circus_tent { background-position: -300px -200px; }
-.emoji-city_dusk { background-position: -300px -220px; }
-.emoji-city_sunset { background-position: -300px -240px; }
-.emoji-cityscape { background-position: -300px -260px; }
-.emoji-cl { background-position: -300px -280px; }
-.emoji-clap { background-position: 0 -300px; }
-.emoji-clap_tone1 { background-position: -20px -300px; }
-.emoji-clap_tone2 { background-position: -40px -300px; }
-.emoji-clap_tone3 { background-position: -60px -300px; }
-.emoji-clap_tone4 { background-position: -80px -300px; }
-.emoji-clap_tone5 { background-position: -100px -300px; }
-.emoji-clapper { background-position: -120px -300px; }
-.emoji-classical_building { background-position: -140px -300px; }
-.emoji-clipboard { background-position: -160px -300px; }
-.emoji-clock { background-position: -180px -300px; }
-.emoji-clock1 { background-position: -200px -300px; }
-.emoji-clock10 { background-position: -220px -300px; }
-.emoji-clock1030 { background-position: -240px -300px; }
-.emoji-clock11 { background-position: -260px -300px; }
-.emoji-clock1130 { background-position: -280px -300px; }
-.emoji-clock12 { background-position: -300px -300px; }
-.emoji-clock1230 { background-position: -320px 0; }
-.emoji-clock130 { background-position: -320px -20px; }
-.emoji-clock2 { background-position: -320px -40px; }
-.emoji-clock230 { background-position: -320px -60px; }
-.emoji-clock3 { background-position: -320px -80px; }
-.emoji-clock330 { background-position: -320px -100px; }
-.emoji-clock4 { background-position: -320px -120px; }
-.emoji-clock430 { background-position: -320px -140px; }
-.emoji-clock5 { background-position: -320px -160px; }
-.emoji-clock530 { background-position: -320px -180px; }
-.emoji-clock6 { background-position: -320px -200px; }
-.emoji-clock630 { background-position: -320px -220px; }
-.emoji-clock7 { background-position: -320px -240px; }
-.emoji-clock730 { background-position: -320px -260px; }
-.emoji-clock8 { background-position: -320px -280px; }
-.emoji-clock830 { background-position: -320px -300px; }
-.emoji-clock9 { background-position: 0 -320px; }
-.emoji-clock930 { background-position: -20px -320px; }
-.emoji-closed_book { background-position: -40px -320px; }
-.emoji-closed_lock_with_key { background-position: -60px -320px; }
-.emoji-closed_umbrella { background-position: -80px -320px; }
-.emoji-cloud { background-position: -100px -320px; }
-.emoji-cloud_lightning { background-position: -120px -320px; }
-.emoji-cloud_rain { background-position: -140px -320px; }
-.emoji-cloud_snow { background-position: -160px -320px; }
-.emoji-cloud_tornado { background-position: -180px -320px; }
-.emoji-clown { background-position: -200px -320px; }
-.emoji-clubs { background-position: -220px -320px; }
-.emoji-cocktail { background-position: -240px -320px; }
-.emoji-coffee { background-position: -260px -320px; }
-.emoji-coffin { background-position: -280px -320px; }
-.emoji-cold_sweat { background-position: -300px -320px; }
-.emoji-comet { background-position: -320px -320px; }
-.emoji-compression { background-position: -340px 0; }
-.emoji-computer { background-position: -340px -20px; }
-.emoji-confetti_ball { background-position: -340px -40px; }
-.emoji-confounded { background-position: -340px -60px; }
-.emoji-confused { background-position: -340px -80px; }
-.emoji-congratulations { background-position: -340px -100px; }
-.emoji-construction { background-position: -340px -120px; }
-.emoji-construction_site { background-position: -340px -140px; }
-.emoji-construction_worker { background-position: -340px -160px; }
-.emoji-construction_worker_tone1 { background-position: -340px -180px; }
-.emoji-construction_worker_tone2 { background-position: -340px -200px; }
-.emoji-construction_worker_tone3 { background-position: -340px -220px; }
-.emoji-construction_worker_tone4 { background-position: -340px -240px; }
-.emoji-construction_worker_tone5 { background-position: -340px -260px; }
-.emoji-control_knobs { background-position: -340px -280px; }
-.emoji-convenience_store { background-position: -340px -300px; }
-.emoji-cookie { background-position: -340px -320px; }
-.emoji-cooking { background-position: 0 -340px; }
-.emoji-cool { background-position: -20px -340px; }
-.emoji-cop { background-position: -40px -340px; }
-.emoji-cop_tone1 { background-position: -60px -340px; }
-.emoji-cop_tone2 { background-position: -80px -340px; }
-.emoji-cop_tone3 { background-position: -100px -340px; }
-.emoji-cop_tone4 { background-position: -120px -340px; }
-.emoji-cop_tone5 { background-position: -140px -340px; }
-.emoji-copyright { background-position: -160px -340px; }
-.emoji-corn { background-position: -180px -340px; }
-.emoji-couch { background-position: -200px -340px; }
-.emoji-couple { background-position: -220px -340px; }
-.emoji-couple_mm { background-position: -240px -340px; }
-.emoji-couple_with_heart { background-position: -260px -340px; }
-.emoji-couple_ww { background-position: -280px -340px; }
-.emoji-couplekiss { background-position: -300px -340px; }
-.emoji-cow { background-position: -320px -340px; }
-.emoji-cow2 { background-position: -340px -340px; }
-.emoji-cowboy { background-position: -360px 0; }
-.emoji-crab { background-position: -360px -20px; }
-.emoji-crayon { background-position: -360px -40px; }
-.emoji-credit_card { background-position: -360px -60px; }
-.emoji-crescent_moon { background-position: -360px -80px; }
-.emoji-cricket { background-position: -360px -100px; }
-.emoji-crocodile { background-position: -360px -120px; }
-.emoji-croissant { background-position: -360px -140px; }
-.emoji-cross { background-position: -360px -160px; }
-.emoji-crossed_flags { background-position: -360px -180px; }
-.emoji-crossed_swords { background-position: -360px -200px; }
-.emoji-crown { background-position: -360px -220px; }
-.emoji-cruise_ship { background-position: -360px -240px; }
-.emoji-cry { background-position: -360px -260px; }
-.emoji-crying_cat_face { background-position: -360px -280px; }
-.emoji-crystal_ball { background-position: -360px -300px; }
-.emoji-cucumber { background-position: -360px -320px; }
-.emoji-cupid { background-position: -360px -340px; }
-.emoji-curly_loop { background-position: 0 -360px; }
-.emoji-currency_exchange { background-position: -20px -360px; }
-.emoji-curry { background-position: -40px -360px; }
-.emoji-custard { background-position: -60px -360px; }
-.emoji-customs { background-position: -80px -360px; }
-.emoji-cyclone { background-position: -100px -360px; }
-.emoji-dagger { background-position: -120px -360px; }
-.emoji-dancer { background-position: -140px -360px; }
-.emoji-dancer_tone1 { background-position: -160px -360px; }
-.emoji-dancer_tone2 { background-position: -180px -360px; }
-.emoji-dancer_tone3 { background-position: -200px -360px; }
-.emoji-dancer_tone4 { background-position: -220px -360px; }
-.emoji-dancer_tone5 { background-position: -240px -360px; }
-.emoji-dancers { background-position: -260px -360px; }
-.emoji-dango { background-position: -280px -360px; }
-.emoji-dark_sunglasses { background-position: -300px -360px; }
-.emoji-dart { background-position: -320px -360px; }
-.emoji-dash { background-position: -340px -360px; }
-.emoji-date { background-position: -360px -360px; }
-.emoji-deciduous_tree { background-position: -380px 0; }
-.emoji-deer { background-position: -380px -20px; }
-.emoji-department_store { background-position: -380px -40px; }
-.emoji-desert { background-position: -380px -60px; }
-.emoji-desktop { background-position: -380px -80px; }
-.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; }
-.emoji-diamonds { background-position: -380px -120px; }
-.emoji-disappointed { background-position: -380px -140px; }
-.emoji-disappointed_relieved { background-position: -380px -160px; }
-.emoji-dividers { background-position: -380px -180px; }
-.emoji-dizzy { background-position: -380px -200px; }
-.emoji-dizzy_face { background-position: -380px -220px; }
-.emoji-do_not_litter { background-position: -380px -240px; }
-.emoji-dog { background-position: -380px -260px; }
-.emoji-dog2 { background-position: -380px -280px; }
-.emoji-dollar { background-position: -380px -300px; }
-.emoji-dolls { background-position: -380px -320px; }
-.emoji-dolphin { background-position: -380px -340px; }
-.emoji-door { background-position: -380px -360px; }
-.emoji-doughnut { background-position: 0 -380px; }
-.emoji-dove { background-position: -20px -380px; }
-.emoji-dragon { background-position: -40px -380px; }
-.emoji-dragon_face { background-position: -60px -380px; }
-.emoji-dress { background-position: -80px -380px; }
-.emoji-dromedary_camel { background-position: -100px -380px; }
-.emoji-drooling_face { background-position: -120px -380px; }
-.emoji-droplet { background-position: -140px -380px; }
-.emoji-drum { background-position: -160px -380px; }
-.emoji-duck { background-position: -180px -380px; }
-.emoji-dvd { background-position: -200px -380px; }
-.emoji-e-mail { background-position: -220px -380px; }
-.emoji-eagle { background-position: -240px -380px; }
-.emoji-ear { background-position: -260px -380px; }
-.emoji-ear_of_rice { background-position: -280px -380px; }
-.emoji-ear_tone1 { background-position: -300px -380px; }
-.emoji-ear_tone2 { background-position: -320px -380px; }
-.emoji-ear_tone3 { background-position: -340px -380px; }
-.emoji-ear_tone4 { background-position: -360px -380px; }
-.emoji-ear_tone5 { background-position: -380px -380px; }
-.emoji-earth_africa { background-position: -400px 0; }
-.emoji-earth_americas { background-position: -400px -20px; }
-.emoji-earth_asia { background-position: -400px -40px; }
-.emoji-egg { background-position: -400px -60px; }
-.emoji-eggplant { background-position: -400px -80px; }
-.emoji-eight { background-position: -400px -100px; }
-.emoji-eight_pointed_black_star { background-position: -400px -120px; }
-.emoji-eight_spoked_asterisk { background-position: -400px -140px; }
-.emoji-eject { background-position: -400px -160px; }
-.emoji-electric_plug { background-position: -400px -180px; }
-.emoji-elephant { background-position: -400px -200px; }
-.emoji-end { background-position: -400px -220px; }
-.emoji-envelope { background-position: -400px -240px; }
-.emoji-envelope_with_arrow { background-position: -400px -260px; }
-.emoji-euro { background-position: -400px -280px; }
-.emoji-european_castle { background-position: -400px -300px; }
-.emoji-european_post_office { background-position: -400px -320px; }
-.emoji-evergreen_tree { background-position: -400px -340px; }
-.emoji-exclamation { background-position: -400px -360px; }
-.emoji-expressionless { background-position: -400px -380px; }
-.emoji-eye { background-position: 0 -400px; }
-.emoji-eye_in_speech_bubble { background-position: -20px -400px; }
-.emoji-eyeglasses { background-position: -40px -400px; }
-.emoji-eyes { background-position: -60px -400px; }
-.emoji-face_palm { background-position: -80px -400px; }
-.emoji-face_palm_tone1 { background-position: -100px -400px; }
-.emoji-face_palm_tone2 { background-position: -120px -400px; }
-.emoji-face_palm_tone3 { background-position: -140px -400px; }
-.emoji-face_palm_tone4 { background-position: -160px -400px; }
-.emoji-face_palm_tone5 { background-position: -180px -400px; }
-.emoji-factory { background-position: -200px -400px; }
-.emoji-fallen_leaf { background-position: -220px -400px; }
-.emoji-family { background-position: -240px -400px; }
-.emoji-family_mmb { background-position: -260px -400px; }
-.emoji-family_mmbb { background-position: -280px -400px; }
-.emoji-family_mmg { background-position: -300px -400px; }
-.emoji-family_mmgb { background-position: -320px -400px; }
-.emoji-family_mmgg { background-position: -340px -400px; }
-.emoji-family_mwbb { background-position: -360px -400px; }
-.emoji-family_mwg { background-position: -380px -400px; }
-.emoji-family_mwgb { background-position: -400px -400px; }
-.emoji-family_mwgg { background-position: -420px 0; }
-.emoji-family_wwb { background-position: -420px -20px; }
-.emoji-family_wwbb { background-position: -420px -40px; }
-.emoji-family_wwg { background-position: -420px -60px; }
-.emoji-family_wwgb { background-position: -420px -80px; }
-.emoji-family_wwgg { background-position: -420px -100px; }
-.emoji-fast_forward { background-position: -420px -120px; }
-.emoji-fax { background-position: -420px -140px; }
-.emoji-fearful { background-position: -420px -160px; }
-.emoji-feet { background-position: -420px -180px; }
-.emoji-fencer { background-position: -420px -200px; }
-.emoji-ferris_wheel { background-position: -420px -220px; }
-.emoji-ferry { background-position: -420px -240px; }
-.emoji-field_hockey { background-position: -420px -260px; }
-.emoji-file_cabinet { background-position: -420px -280px; }
-.emoji-file_folder { background-position: -420px -300px; }
-.emoji-film_frames { background-position: -420px -320px; }
-.emoji-fingers_crossed { background-position: -420px -340px; }
-.emoji-fingers_crossed_tone1 { background-position: -420px -360px; }
-.emoji-fingers_crossed_tone2 { background-position: -420px -380px; }
-.emoji-fingers_crossed_tone3 { background-position: -420px -400px; }
-.emoji-fingers_crossed_tone4 { background-position: 0 -420px; }
-.emoji-fingers_crossed_tone5 { background-position: -20px -420px; }
-.emoji-fire { background-position: -40px -420px; }
-.emoji-fire_engine { background-position: -60px -420px; }
-.emoji-fireworks { background-position: -80px -420px; }
-.emoji-first_place { background-position: -100px -420px; }
-.emoji-first_quarter_moon { background-position: -120px -420px; }
-.emoji-first_quarter_moon_with_face { background-position: -140px -420px; }
-.emoji-fish { background-position: -160px -420px; }
-.emoji-fish_cake { background-position: -180px -420px; }
-.emoji-fishing_pole_and_fish { background-position: -200px -420px; }
-.emoji-fist { background-position: -220px -420px; }
-.emoji-fist_tone1 { background-position: -240px -420px; }
-.emoji-fist_tone2 { background-position: -260px -420px; }
-.emoji-fist_tone3 { background-position: -280px -420px; }
-.emoji-fist_tone4 { background-position: -300px -420px; }
-.emoji-fist_tone5 { background-position: -320px -420px; }
-.emoji-five { background-position: -340px -420px; }
-.emoji-flag_ac { background-position: -360px -420px; }
-.emoji-flag_ad { background-position: -380px -420px; }
-.emoji-flag_ae { background-position: -400px -420px; }
-.emoji-flag_af { background-position: -420px -420px; }
-.emoji-flag_ag { background-position: -440px 0; }
-.emoji-flag_ai { background-position: -440px -20px; }
-.emoji-flag_al { background-position: -440px -40px; }
-.emoji-flag_am { background-position: -440px -60px; }
-.emoji-flag_ao { background-position: -440px -80px; }
-.emoji-flag_aq { background-position: -440px -100px; }
-.emoji-flag_ar { background-position: -440px -120px; }
-.emoji-flag_as { background-position: -440px -140px; }
-.emoji-flag_at { background-position: -440px -160px; }
-.emoji-flag_au { background-position: -440px -180px; }
-.emoji-flag_aw { background-position: -440px -200px; }
-.emoji-flag_ax { background-position: -440px -220px; }
-.emoji-flag_az { background-position: -440px -240px; }
-.emoji-flag_ba { background-position: -440px -260px; }
-.emoji-flag_bb { background-position: -440px -280px; }
-.emoji-flag_bd { background-position: -440px -300px; }
-.emoji-flag_be { background-position: -440px -320px; }
-.emoji-flag_bf { background-position: -440px -340px; }
-.emoji-flag_bg { background-position: -440px -360px; }
-.emoji-flag_bh { background-position: -440px -380px; }
-.emoji-flag_bi { background-position: -440px -400px; }
-.emoji-flag_bj { background-position: -440px -420px; }
-.emoji-flag_bl { background-position: 0 -440px; }
-.emoji-flag_black { background-position: -20px -440px; }
-.emoji-flag_bm { background-position: -40px -440px; }
-.emoji-flag_bn { background-position: -60px -440px; }
-.emoji-flag_bo { background-position: -80px -440px; }
-.emoji-flag_bq { background-position: -100px -440px; }
-.emoji-flag_br { background-position: -120px -440px; }
-.emoji-flag_bs { background-position: -140px -440px; }
-.emoji-flag_bt { background-position: -160px -440px; }
-.emoji-flag_bv { background-position: -180px -440px; }
-.emoji-flag_bw { background-position: -200px -440px; }
-.emoji-flag_by { background-position: -220px -440px; }
-.emoji-flag_bz { background-position: -240px -440px; }
-.emoji-flag_ca { background-position: -260px -440px; }
-.emoji-flag_cc { background-position: -280px -440px; }
-.emoji-flag_cd { background-position: -300px -440px; }
-.emoji-flag_cf { background-position: -320px -440px; }
-.emoji-flag_cg { background-position: -340px -440px; }
-.emoji-flag_ch { background-position: -360px -440px; }
-.emoji-flag_ci { background-position: -380px -440px; }
-.emoji-flag_ck { background-position: -400px -440px; }
-.emoji-flag_cl { background-position: -420px -440px; }
-.emoji-flag_cm { background-position: -440px -440px; }
-.emoji-flag_cn { background-position: -460px 0; }
-.emoji-flag_co { background-position: -460px -20px; }
-.emoji-flag_cp { background-position: -460px -40px; }
-.emoji-flag_cr { background-position: -460px -60px; }
-.emoji-flag_cu { background-position: -460px -80px; }
-.emoji-flag_cv { background-position: -460px -100px; }
-.emoji-flag_cw { background-position: -460px -120px; }
-.emoji-flag_cx { background-position: -460px -140px; }
-.emoji-flag_cy { background-position: -460px -160px; }
-.emoji-flag_cz { background-position: -460px -180px; }
-.emoji-flag_de { background-position: -460px -200px; }
-.emoji-flag_dg { background-position: -460px -220px; }
-.emoji-flag_dj { background-position: -460px -240px; }
-.emoji-flag_dk { background-position: -460px -260px; }
-.emoji-flag_dm { background-position: -460px -280px; }
-.emoji-flag_do { background-position: -460px -300px; }
-.emoji-flag_dz { background-position: -460px -320px; }
-.emoji-flag_ea { background-position: -460px -340px; }
-.emoji-flag_ec { background-position: -460px -360px; }
-.emoji-flag_ee { background-position: -460px -380px; }
-.emoji-flag_eg { background-position: -460px -400px; }
-.emoji-flag_eh { background-position: -460px -420px; }
-.emoji-flag_er { background-position: -460px -440px; }
-.emoji-flag_es { background-position: 0 -460px; }
-.emoji-flag_et { background-position: -20px -460px; }
-.emoji-flag_eu { background-position: -40px -460px; }
-.emoji-flag_fi { background-position: -60px -460px; }
-.emoji-flag_fj { background-position: -80px -460px; }
-.emoji-flag_fk { background-position: -100px -460px; }
-.emoji-flag_fm { background-position: -120px -460px; }
-.emoji-flag_fo { background-position: -140px -460px; }
-.emoji-flag_fr { background-position: -160px -460px; }
-.emoji-flag_ga { background-position: -180px -460px; }
-.emoji-flag_gb { background-position: -200px -460px; }
-.emoji-flag_gd { background-position: -220px -460px; }
-.emoji-flag_ge { background-position: -240px -460px; }
-.emoji-flag_gf { background-position: -260px -460px; }
-.emoji-flag_gg { background-position: -280px -460px; }
-.emoji-flag_gh { background-position: -300px -460px; }
-.emoji-flag_gi { background-position: -320px -460px; }
-.emoji-flag_gl { background-position: -340px -460px; }
-.emoji-flag_gm { background-position: -360px -460px; }
-.emoji-flag_gn { background-position: -380px -460px; }
-.emoji-flag_gp { background-position: -400px -460px; }
-.emoji-flag_gq { background-position: -420px -460px; }
-.emoji-flag_gr { background-position: -440px -460px; }
-.emoji-flag_gs { background-position: -460px -460px; }
-.emoji-flag_gt { background-position: -480px 0; }
-.emoji-flag_gu { background-position: -480px -20px; }
-.emoji-flag_gw { background-position: -480px -40px; }
-.emoji-flag_gy { background-position: -480px -60px; }
-.emoji-flag_hk { background-position: -480px -80px; }
-.emoji-flag_hm { background-position: -480px -100px; }
-.emoji-flag_hn { background-position: -480px -120px; }
-.emoji-flag_hr { background-position: -480px -140px; }
-.emoji-flag_ht { background-position: -480px -160px; }
-.emoji-flag_hu { background-position: -480px -180px; }
-.emoji-flag_ic { background-position: -480px -200px; }
-.emoji-flag_id { background-position: -480px -220px; }
-.emoji-flag_ie { background-position: -480px -240px; }
-.emoji-flag_il { background-position: -480px -260px; }
-.emoji-flag_im { background-position: -480px -280px; }
-.emoji-flag_in { background-position: -480px -300px; }
-.emoji-flag_io { background-position: -480px -320px; }
-.emoji-flag_iq { background-position: -480px -340px; }
-.emoji-flag_ir { background-position: -480px -360px; }
-.emoji-flag_is { background-position: -480px -380px; }
-.emoji-flag_it { background-position: -480px -400px; }
-.emoji-flag_je { background-position: -480px -420px; }
-.emoji-flag_jm { background-position: -480px -440px; }
-.emoji-flag_jo { background-position: -480px -460px; }
-.emoji-flag_jp { background-position: 0 -480px; }
-.emoji-flag_ke { background-position: -20px -480px; }
-.emoji-flag_kg { background-position: -40px -480px; }
-.emoji-flag_kh { background-position: -60px -480px; }
-.emoji-flag_ki { background-position: -80px -480px; }
-.emoji-flag_km { background-position: -100px -480px; }
-.emoji-flag_kn { background-position: -120px -480px; }
-.emoji-flag_kp { background-position: -140px -480px; }
-.emoji-flag_kr { background-position: -160px -480px; }
-.emoji-flag_kw { background-position: -180px -480px; }
-.emoji-flag_ky { background-position: -200px -480px; }
-.emoji-flag_kz { background-position: -220px -480px; }
-.emoji-flag_la { background-position: -240px -480px; }
-.emoji-flag_lb { background-position: -260px -480px; }
-.emoji-flag_lc { background-position: -280px -480px; }
-.emoji-flag_li { background-position: -300px -480px; }
-.emoji-flag_lk { background-position: -320px -480px; }
-.emoji-flag_lr { background-position: -340px -480px; }
-.emoji-flag_ls { background-position: -360px -480px; }
-.emoji-flag_lt { background-position: -380px -480px; }
-.emoji-flag_lu { background-position: -400px -480px; }
-.emoji-flag_lv { background-position: -420px -480px; }
-.emoji-flag_ly { background-position: -440px -480px; }
-.emoji-flag_ma { background-position: -460px -480px; }
-.emoji-flag_mc { background-position: -480px -480px; }
-.emoji-flag_md { background-position: -500px 0; }
-.emoji-flag_me { background-position: -500px -20px; }
-.emoji-flag_mf { background-position: -500px -40px; }
-.emoji-flag_mg { background-position: -500px -60px; }
-.emoji-flag_mh { background-position: -500px -80px; }
-.emoji-flag_mk { background-position: -500px -100px; }
-.emoji-flag_ml { background-position: -500px -120px; }
-.emoji-flag_mm { background-position: -500px -140px; }
-.emoji-flag_mn { background-position: -500px -160px; }
-.emoji-flag_mo { background-position: -500px -180px; }
-.emoji-flag_mp { background-position: -500px -200px; }
-.emoji-flag_mq { background-position: -500px -220px; }
-.emoji-flag_mr { background-position: -500px -240px; }
-.emoji-flag_ms { background-position: -500px -260px; }
-.emoji-flag_mt { background-position: -500px -280px; }
-.emoji-flag_mu { background-position: -500px -300px; }
-.emoji-flag_mv { background-position: -500px -320px; }
-.emoji-flag_mw { background-position: -500px -340px; }
-.emoji-flag_mx { background-position: -500px -360px; }
-.emoji-flag_my { background-position: -500px -380px; }
-.emoji-flag_mz { background-position: -500px -400px; }
-.emoji-flag_na { background-position: -500px -420px; }
-.emoji-flag_nc { background-position: -500px -440px; }
-.emoji-flag_ne { background-position: -500px -460px; }
-.emoji-flag_nf { background-position: -500px -480px; }
-.emoji-flag_ng { background-position: 0 -500px; }
-.emoji-flag_ni { background-position: -20px -500px; }
-.emoji-flag_nl { background-position: -40px -500px; }
-.emoji-flag_no { background-position: -60px -500px; }
-.emoji-flag_np { background-position: -80px -500px; }
-.emoji-flag_nr { background-position: -100px -500px; }
-.emoji-flag_nu { background-position: -120px -500px; }
-.emoji-flag_nz { background-position: -140px -500px; }
-.emoji-flag_om { background-position: -160px -500px; }
-.emoji-flag_pa { background-position: -180px -500px; }
-.emoji-flag_pe { background-position: -200px -500px; }
-.emoji-flag_pf { background-position: -220px -500px; }
-.emoji-flag_pg { background-position: -240px -500px; }
-.emoji-flag_ph { background-position: -260px -500px; }
-.emoji-flag_pk { background-position: -280px -500px; }
-.emoji-flag_pl { background-position: -300px -500px; }
-.emoji-flag_pm { background-position: -320px -500px; }
-.emoji-flag_pn { background-position: -340px -500px; }
-.emoji-flag_pr { background-position: -360px -500px; }
-.emoji-flag_ps { background-position: -380px -500px; }
-.emoji-flag_pt { background-position: -400px -500px; }
-.emoji-flag_pw { background-position: -420px -500px; }
-.emoji-flag_py { background-position: -440px -500px; }
-.emoji-flag_qa { background-position: -460px -500px; }
-.emoji-flag_re { background-position: -480px -500px; }
-.emoji-flag_ro { background-position: -500px -500px; }
-.emoji-flag_rs { background-position: -520px 0; }
-.emoji-flag_ru { background-position: -520px -20px; }
-.emoji-flag_rw { background-position: -520px -40px; }
-.emoji-flag_sa { background-position: -520px -60px; }
-.emoji-flag_sb { background-position: -520px -80px; }
-.emoji-flag_sc { background-position: -520px -100px; }
-.emoji-flag_sd { background-position: -520px -120px; }
-.emoji-flag_se { background-position: -520px -140px; }
-.emoji-flag_sg { background-position: -520px -160px; }
-.emoji-flag_sh { background-position: -520px -180px; }
-.emoji-flag_si { background-position: -520px -200px; }
-.emoji-flag_sj { background-position: -520px -220px; }
-.emoji-flag_sk { background-position: -520px -240px; }
-.emoji-flag_sl { background-position: -520px -260px; }
-.emoji-flag_sm { background-position: -520px -280px; }
-.emoji-flag_sn { background-position: -520px -300px; }
-.emoji-flag_so { background-position: -520px -320px; }
-.emoji-flag_sr { background-position: -520px -340px; }
-.emoji-flag_ss { background-position: -520px -360px; }
-.emoji-flag_st { background-position: -520px -380px; }
-.emoji-flag_sv { background-position: -520px -400px; }
-.emoji-flag_sx { background-position: -520px -420px; }
-.emoji-flag_sy { background-position: -520px -440px; }
-.emoji-flag_sz { background-position: -520px -460px; }
-.emoji-flag_ta { background-position: -520px -480px; }
-.emoji-flag_tc { background-position: -520px -500px; }
-.emoji-flag_td { background-position: 0 -520px; }
-.emoji-flag_tf { background-position: -20px -520px; }
-.emoji-flag_tg { background-position: -40px -520px; }
-.emoji-flag_th { background-position: -60px -520px; }
-.emoji-flag_tj { background-position: -80px -520px; }
-.emoji-flag_tk { background-position: -100px -520px; }
-.emoji-flag_tl { background-position: -120px -520px; }
-.emoji-flag_tm { background-position: -140px -520px; }
-.emoji-flag_tn { background-position: -160px -520px; }
-.emoji-flag_to { background-position: -180px -520px; }
-.emoji-flag_tr { background-position: -200px -520px; }
-.emoji-flag_tt { background-position: -220px -520px; }
-.emoji-flag_tv { background-position: -240px -520px; }
-.emoji-flag_tw { background-position: -260px -520px; }
-.emoji-flag_tz { background-position: -280px -520px; }
-.emoji-flag_ua { background-position: -300px -520px; }
-.emoji-flag_ug { background-position: -320px -520px; }
-.emoji-flag_um { background-position: -340px -520px; }
-.emoji-flag_us { background-position: -360px -520px; }
-.emoji-flag_uy { background-position: -380px -520px; }
-.emoji-flag_uz { background-position: -400px -520px; }
-.emoji-flag_va { background-position: -420px -520px; }
-.emoji-flag_vc { background-position: -440px -520px; }
-.emoji-flag_ve { background-position: -460px -520px; }
-.emoji-flag_vg { background-position: -480px -520px; }
-.emoji-flag_vi { background-position: -500px -520px; }
-.emoji-flag_vn { background-position: -520px -520px; }
-.emoji-flag_vu { background-position: -540px 0; }
-.emoji-flag_wf { background-position: -540px -20px; }
-.emoji-flag_white { background-position: -540px -40px; }
-.emoji-flag_ws { background-position: -540px -60px; }
-.emoji-flag_xk { background-position: -540px -80px; }
-.emoji-flag_ye { background-position: -540px -100px; }
-.emoji-flag_yt { background-position: -540px -120px; }
-.emoji-flag_za { background-position: -540px -140px; }
-.emoji-flag_zm { background-position: -540px -160px; }
-.emoji-flag_zw { background-position: -540px -180px; }
-.emoji-flags { background-position: -540px -200px; }
-.emoji-flashlight { background-position: -540px -220px; }
-.emoji-fleur-de-lis { background-position: -540px -240px; }
-.emoji-floppy_disk { background-position: -540px -260px; }
-.emoji-flower_playing_cards { background-position: -540px -280px; }
-.emoji-flushed { background-position: -540px -300px; }
-.emoji-fog { background-position: -540px -320px; }
-.emoji-foggy { background-position: -540px -340px; }
-.emoji-football { background-position: -540px -360px; }
-.emoji-footprints { background-position: -540px -380px; }
-.emoji-fork_and_knife { background-position: -540px -400px; }
-.emoji-fork_knife_plate { background-position: -540px -420px; }
-.emoji-fountain { background-position: -540px -440px; }
-.emoji-four { background-position: -540px -460px; }
-.emoji-four_leaf_clover { background-position: -540px -480px; }
-.emoji-fox { background-position: -540px -500px; }
-.emoji-frame_photo { background-position: -540px -520px; }
-.emoji-free { background-position: 0 -540px; }
-.emoji-french_bread { background-position: -20px -540px; }
-.emoji-fried_shrimp { background-position: -40px -540px; }
-.emoji-fries { background-position: -60px -540px; }
-.emoji-frog { background-position: -80px -540px; }
-.emoji-frowning { background-position: -100px -540px; }
-.emoji-frowning2 { background-position: -120px -540px; }
-.emoji-fuelpump { background-position: -140px -540px; }
-.emoji-full_moon { background-position: -160px -540px; }
-.emoji-full_moon_with_face { background-position: -180px -540px; }
-.emoji-game_die { background-position: -200px -540px; }
-.emoji-gay_pride_flag { background-position: -220px -540px; }
-.emoji-gear { background-position: -240px -540px; }
-.emoji-gem { background-position: -260px -540px; }
-.emoji-gemini { background-position: -280px -540px; }
-.emoji-ghost { background-position: -300px -540px; }
-.emoji-gift { background-position: -320px -540px; }
-.emoji-gift_heart { background-position: -340px -540px; }
-.emoji-girl { background-position: -360px -540px; }
-.emoji-girl_tone1 { background-position: -380px -540px; }
-.emoji-girl_tone2 { background-position: -400px -540px; }
-.emoji-girl_tone3 { background-position: -420px -540px; }
-.emoji-girl_tone4 { background-position: -440px -540px; }
-.emoji-girl_tone5 { background-position: -460px -540px; }
-.emoji-globe_with_meridians { background-position: -480px -540px; }
-.emoji-goal { background-position: -500px -540px; }
-.emoji-goat { background-position: -520px -540px; }
-.emoji-golf { background-position: -540px -540px; }
-.emoji-golfer { background-position: -560px 0; }
-.emoji-gorilla { background-position: -560px -20px; }
-.emoji-grapes { background-position: -560px -40px; }
-.emoji-green_apple { background-position: -560px -60px; }
-.emoji-green_book { background-position: -560px -80px; }
-.emoji-green_heart { background-position: -560px -100px; }
-.emoji-grey_exclamation { background-position: -560px -120px; }
-.emoji-grey_question { background-position: -560px -140px; }
-.emoji-grimacing { background-position: -560px -160px; }
-.emoji-grin { background-position: -560px -180px; }
-.emoji-grinning { background-position: -560px -200px; }
-.emoji-guardsman { background-position: -560px -220px; }
-.emoji-guardsman_tone1 { background-position: -560px -240px; }
-.emoji-guardsman_tone2 { background-position: -560px -260px; }
-.emoji-guardsman_tone3 { background-position: -560px -280px; }
-.emoji-guardsman_tone4 { background-position: -560px -300px; }
-.emoji-guardsman_tone5 { background-position: -560px -320px; }
-.emoji-guitar { background-position: -560px -340px; }
-.emoji-gun { background-position: -560px -360px; }
-.emoji-haircut { background-position: -560px -380px; }
-.emoji-haircut_tone1 { background-position: -560px -400px; }
-.emoji-haircut_tone2 { background-position: -560px -420px; }
-.emoji-haircut_tone3 { background-position: -560px -440px; }
-.emoji-haircut_tone4 { background-position: -560px -460px; }
-.emoji-haircut_tone5 { background-position: -560px -480px; }
-.emoji-hamburger { background-position: -560px -500px; }
-.emoji-hammer { background-position: -560px -520px; }
-.emoji-hammer_pick { background-position: -560px -540px; }
-.emoji-hamster { background-position: 0 -560px; }
-.emoji-hand_splayed { background-position: -20px -560px; }
-.emoji-hand_splayed_tone1 { background-position: -40px -560px; }
-.emoji-hand_splayed_tone2 { background-position: -60px -560px; }
-.emoji-hand_splayed_tone3 { background-position: -80px -560px; }
-.emoji-hand_splayed_tone4 { background-position: -100px -560px; }
-.emoji-hand_splayed_tone5 { background-position: -120px -560px; }
-.emoji-handbag { background-position: -140px -560px; }
-.emoji-handball { background-position: -160px -560px; }
-.emoji-handball_tone1 { background-position: -180px -560px; }
-.emoji-handball_tone2 { background-position: -200px -560px; }
-.emoji-handball_tone3 { background-position: -220px -560px; }
-.emoji-handball_tone4 { background-position: -240px -560px; }
-.emoji-handball_tone5 { background-position: -260px -560px; }
-.emoji-handshake { background-position: -280px -560px; }
-.emoji-handshake_tone1 { background-position: -300px -560px; }
-.emoji-handshake_tone2 { background-position: -320px -560px; }
-.emoji-handshake_tone3 { background-position: -340px -560px; }
-.emoji-handshake_tone4 { background-position: -360px -560px; }
-.emoji-handshake_tone5 { background-position: -380px -560px; }
-.emoji-hash { background-position: -400px -560px; }
-.emoji-hatched_chick { background-position: -420px -560px; }
-.emoji-hatching_chick { background-position: -440px -560px; }
-.emoji-head_bandage { background-position: -460px -560px; }
-.emoji-headphones { background-position: -480px -560px; }
-.emoji-hear_no_evil { background-position: -500px -560px; }
-.emoji-heart { background-position: -520px -560px; }
-.emoji-heart_decoration { background-position: -540px -560px; }
-.emoji-heart_exclamation { background-position: -560px -560px; }
-.emoji-heart_eyes { background-position: -580px 0; }
-.emoji-heart_eyes_cat { background-position: -580px -20px; }
-.emoji-heartbeat { background-position: -580px -40px; }
-.emoji-heartpulse { background-position: -580px -60px; }
-.emoji-hearts { background-position: -580px -80px; }
-.emoji-heavy_check_mark { background-position: -580px -100px; }
-.emoji-heavy_division_sign { background-position: -580px -120px; }
-.emoji-heavy_dollar_sign { background-position: -580px -140px; }
-.emoji-heavy_minus_sign { background-position: -580px -160px; }
-.emoji-heavy_multiplication_x { background-position: -580px -180px; }
-.emoji-heavy_plus_sign { background-position: -580px -200px; }
-.emoji-helicopter { background-position: -580px -220px; }
-.emoji-helmet_with_cross { background-position: -580px -240px; }
-.emoji-herb { background-position: -580px -260px; }
-.emoji-hibiscus { background-position: -580px -280px; }
-.emoji-high_brightness { background-position: -580px -300px; }
-.emoji-high_heel { background-position: -580px -320px; }
-.emoji-hockey { background-position: -580px -340px; }
-.emoji-hole { background-position: -580px -360px; }
-.emoji-homes { background-position: -580px -380px; }
-.emoji-honey_pot { background-position: -580px -400px; }
-.emoji-horse { background-position: -580px -420px; }
-.emoji-horse_racing { background-position: -580px -440px; }
-.emoji-horse_racing_tone1 { background-position: -580px -460px; }
-.emoji-horse_racing_tone2 { background-position: -580px -480px; }
-.emoji-horse_racing_tone3 { background-position: -580px -500px; }
-.emoji-horse_racing_tone4 { background-position: -580px -520px; }
-.emoji-horse_racing_tone5 { background-position: -580px -540px; }
-.emoji-hospital { background-position: -580px -560px; }
-.emoji-hot_pepper { background-position: 0 -580px; }
-.emoji-hotdog { background-position: -20px -580px; }
-.emoji-hotel { background-position: -40px -580px; }
-.emoji-hotsprings { background-position: -60px -580px; }
-.emoji-hourglass { background-position: -80px -580px; }
-.emoji-hourglass_flowing_sand { background-position: -100px -580px; }
-.emoji-house { background-position: -120px -580px; }
-.emoji-house_abandoned { background-position: -140px -580px; }
-.emoji-house_with_garden { background-position: -160px -580px; }
-.emoji-hugging { background-position: -180px -580px; }
-.emoji-hushed { background-position: -200px -580px; }
-.emoji-ice_cream { background-position: -220px -580px; }
-.emoji-ice_skate { background-position: -240px -580px; }
-.emoji-icecream { background-position: -260px -580px; }
-.emoji-id { background-position: -280px -580px; }
-.emoji-ideograph_advantage { background-position: -300px -580px; }
-.emoji-imp { background-position: -320px -580px; }
-.emoji-inbox_tray { background-position: -340px -580px; }
-.emoji-incoming_envelope { background-position: -360px -580px; }
-.emoji-information_desk_person { background-position: -380px -580px; }
-.emoji-information_desk_person_tone1 { background-position: -400px -580px; }
-.emoji-information_desk_person_tone2 { background-position: -420px -580px; }
-.emoji-information_desk_person_tone3 { background-position: -440px -580px; }
-.emoji-information_desk_person_tone4 { background-position: -460px -580px; }
-.emoji-information_desk_person_tone5 { background-position: -480px -580px; }
-.emoji-information_source { background-position: -500px -580px; }
-.emoji-innocent { background-position: -520px -580px; }
-.emoji-interrobang { background-position: -540px -580px; }
-.emoji-iphone { background-position: -560px -580px; }
-.emoji-island { background-position: -580px -580px; }
-.emoji-izakaya_lantern { background-position: -600px 0; }
-.emoji-jack_o_lantern { background-position: -600px -20px; }
-.emoji-japan { background-position: -600px -40px; }
-.emoji-japanese_castle { background-position: -600px -60px; }
-.emoji-japanese_goblin { background-position: -600px -80px; }
-.emoji-japanese_ogre { background-position: -600px -100px; }
-.emoji-jeans { background-position: -600px -120px; }
-.emoji-joy { background-position: -600px -140px; }
-.emoji-joy_cat { background-position: -600px -160px; }
-.emoji-joystick { background-position: -600px -180px; }
-.emoji-juggling { background-position: -600px -200px; }
-.emoji-juggling_tone1 { background-position: -600px -220px; }
-.emoji-juggling_tone2 { background-position: -600px -240px; }
-.emoji-juggling_tone3 { background-position: -600px -260px; }
-.emoji-juggling_tone4 { background-position: -600px -280px; }
-.emoji-juggling_tone5 { background-position: -600px -300px; }
-.emoji-kaaba { background-position: -600px -320px; }
-.emoji-key { background-position: -600px -340px; }
-.emoji-key2 { background-position: -600px -360px; }
-.emoji-keyboard { background-position: -600px -380px; }
-.emoji-kimono { background-position: -600px -400px; }
-.emoji-kiss { background-position: -600px -420px; }
-.emoji-kiss_mm { background-position: -600px -440px; }
-.emoji-kiss_ww { background-position: -600px -460px; }
-.emoji-kissing { background-position: -600px -480px; }
-.emoji-kissing_cat { background-position: -600px -500px; }
-.emoji-kissing_closed_eyes { background-position: -600px -520px; }
-.emoji-kissing_heart { background-position: -600px -540px; }
-.emoji-kissing_smiling_eyes { background-position: -600px -560px; }
-.emoji-kiwi { background-position: -600px -580px; }
-.emoji-knife { background-position: 0 -600px; }
-.emoji-koala { background-position: -20px -600px; }
-.emoji-koko { background-position: -40px -600px; }
-.emoji-label { background-position: -60px -600px; }
-.emoji-large_blue_circle { background-position: -80px -600px; }
-.emoji-large_blue_diamond { background-position: -100px -600px; }
-.emoji-large_orange_diamond { background-position: -120px -600px; }
-.emoji-last_quarter_moon { background-position: -140px -600px; }
-.emoji-last_quarter_moon_with_face { background-position: -160px -600px; }
-.emoji-laughing { background-position: -180px -600px; }
-.emoji-leaves { background-position: -200px -600px; }
-.emoji-ledger { background-position: -220px -600px; }
-.emoji-left_facing_fist { background-position: -240px -600px; }
-.emoji-left_facing_fist_tone1 { background-position: -260px -600px; }
-.emoji-left_facing_fist_tone2 { background-position: -280px -600px; }
-.emoji-left_facing_fist_tone3 { background-position: -300px -600px; }
-.emoji-left_facing_fist_tone4 { background-position: -320px -600px; }
-.emoji-left_facing_fist_tone5 { background-position: -340px -600px; }
-.emoji-left_luggage { background-position: -360px -600px; }
-.emoji-left_right_arrow { background-position: -380px -600px; }
-.emoji-leftwards_arrow_with_hook { background-position: -400px -600px; }
-.emoji-lemon { background-position: -420px -600px; }
-.emoji-leo { background-position: -440px -600px; }
-.emoji-leopard { background-position: -460px -600px; }
-.emoji-level_slider { background-position: -480px -600px; }
-.emoji-levitate { background-position: -500px -600px; }
-.emoji-libra { background-position: -520px -600px; }
-.emoji-lifter { background-position: -540px -600px; }
-.emoji-lifter_tone1 { background-position: -560px -600px; }
-.emoji-lifter_tone2 { background-position: -580px -600px; }
-.emoji-lifter_tone3 { background-position: -600px -600px; }
-.emoji-lifter_tone4 { background-position: -620px 0; }
-.emoji-lifter_tone5 { background-position: -620px -20px; }
-.emoji-light_rail { background-position: -620px -40px; }
-.emoji-link { background-position: -620px -60px; }
-.emoji-lion_face { background-position: -620px -80px; }
-.emoji-lips { background-position: -620px -100px; }
-.emoji-lipstick { background-position: -620px -120px; }
-.emoji-lizard { background-position: -620px -140px; }
-.emoji-lock { background-position: -620px -160px; }
-.emoji-lock_with_ink_pen { background-position: -620px -180px; }
-.emoji-lollipop { background-position: -620px -200px; }
-.emoji-loop { background-position: -620px -220px; }
-.emoji-loud_sound { background-position: -620px -240px; }
-.emoji-loudspeaker { background-position: -620px -260px; }
-.emoji-love_hotel { background-position: -620px -280px; }
-.emoji-love_letter { background-position: -620px -300px; }
-.emoji-low_brightness { background-position: -620px -320px; }
-.emoji-lying_face { background-position: -620px -340px; }
-.emoji-m { background-position: -620px -360px; }
-.emoji-mag { background-position: -620px -380px; }
-.emoji-mag_right { background-position: -620px -400px; }
-.emoji-mahjong { background-position: -620px -420px; }
-.emoji-mailbox { background-position: -620px -440px; }
-.emoji-mailbox_closed { background-position: -620px -460px; }
-.emoji-mailbox_with_mail { background-position: -620px -480px; }
-.emoji-mailbox_with_no_mail { background-position: -620px -500px; }
-.emoji-man { background-position: -620px -520px; }
-.emoji-man_dancing { background-position: -620px -540px; }
-.emoji-man_dancing_tone1 { background-position: -620px -560px; }
-.emoji-man_dancing_tone2 { background-position: -620px -580px; }
-.emoji-man_dancing_tone3 { background-position: -620px -600px; }
-.emoji-man_dancing_tone4 { background-position: 0 -620px; }
-.emoji-man_dancing_tone5 { background-position: -20px -620px; }
-.emoji-man_in_tuxedo { background-position: -40px -620px; }
-.emoji-man_in_tuxedo_tone1 { background-position: -60px -620px; }
-.emoji-man_in_tuxedo_tone2 { background-position: -80px -620px; }
-.emoji-man_in_tuxedo_tone3 { background-position: -100px -620px; }
-.emoji-man_in_tuxedo_tone4 { background-position: -120px -620px; }
-.emoji-man_in_tuxedo_tone5 { background-position: -140px -620px; }
-.emoji-man_tone1 { background-position: -160px -620px; }
-.emoji-man_tone2 { background-position: -180px -620px; }
-.emoji-man_tone3 { background-position: -200px -620px; }
-.emoji-man_tone4 { background-position: -220px -620px; }
-.emoji-man_tone5 { background-position: -240px -620px; }
-.emoji-man_with_gua_pi_mao { background-position: -260px -620px; }
-.emoji-man_with_gua_pi_mao_tone1 { background-position: -280px -620px; }
-.emoji-man_with_gua_pi_mao_tone2 { background-position: -300px -620px; }
-.emoji-man_with_gua_pi_mao_tone3 { background-position: -320px -620px; }
-.emoji-man_with_gua_pi_mao_tone4 { background-position: -340px -620px; }
-.emoji-man_with_gua_pi_mao_tone5 { background-position: -360px -620px; }
-.emoji-man_with_turban { background-position: -380px -620px; }
-.emoji-man_with_turban_tone1 { background-position: -400px -620px; }
-.emoji-man_with_turban_tone2 { background-position: -420px -620px; }
-.emoji-man_with_turban_tone3 { background-position: -440px -620px; }
-.emoji-man_with_turban_tone4 { background-position: -460px -620px; }
-.emoji-man_with_turban_tone5 { background-position: -480px -620px; }
-.emoji-mans_shoe { background-position: -500px -620px; }
-.emoji-map { background-position: -520px -620px; }
-.emoji-maple_leaf { background-position: -540px -620px; }
-.emoji-martial_arts_uniform { background-position: -560px -620px; }
-.emoji-mask { background-position: -580px -620px; }
-.emoji-massage { background-position: -600px -620px; }
-.emoji-massage_tone1 { background-position: -620px -620px; }
-.emoji-massage_tone2 { background-position: -640px 0; }
-.emoji-massage_tone3 { background-position: -640px -20px; }
-.emoji-massage_tone4 { background-position: -640px -40px; }
-.emoji-massage_tone5 { background-position: -640px -60px; }
-.emoji-meat_on_bone { background-position: -640px -80px; }
-.emoji-medal { background-position: -640px -100px; }
-.emoji-mega { background-position: -640px -120px; }
-.emoji-melon { background-position: -640px -140px; }
-.emoji-menorah { background-position: -640px -160px; }
-.emoji-mens { background-position: -640px -180px; }
-.emoji-metal { background-position: -640px -200px; }
-.emoji-metal_tone1 { background-position: -640px -220px; }
-.emoji-metal_tone2 { background-position: -640px -240px; }
-.emoji-metal_tone3 { background-position: -640px -260px; }
-.emoji-metal_tone4 { background-position: -640px -280px; }
-.emoji-metal_tone5 { background-position: -640px -300px; }
-.emoji-metro { background-position: -640px -320px; }
-.emoji-microphone { background-position: -640px -340px; }
-.emoji-microphone2 { background-position: -640px -360px; }
-.emoji-microscope { background-position: -640px -380px; }
-.emoji-middle_finger { background-position: -640px -400px; }
-.emoji-middle_finger_tone1 { background-position: -640px -420px; }
-.emoji-middle_finger_tone2 { background-position: -640px -440px; }
-.emoji-middle_finger_tone3 { background-position: -640px -460px; }
-.emoji-middle_finger_tone4 { background-position: -640px -480px; }
-.emoji-middle_finger_tone5 { background-position: -640px -500px; }
-.emoji-military_medal { background-position: -640px -520px; }
-.emoji-milk { background-position: -640px -540px; }
-.emoji-milky_way { background-position: -640px -560px; }
-.emoji-minibus { background-position: -640px -580px; }
-.emoji-minidisc { background-position: -640px -600px; }
-.emoji-mobile_phone_off { background-position: -640px -620px; }
-.emoji-money_mouth { background-position: 0 -640px; }
-.emoji-money_with_wings { background-position: -20px -640px; }
-.emoji-moneybag { background-position: -40px -640px; }
-.emoji-monkey { background-position: -60px -640px; }
-.emoji-monkey_face { background-position: -80px -640px; }
-.emoji-monorail { background-position: -100px -640px; }
-.emoji-mortar_board { background-position: -120px -640px; }
-.emoji-mosque { background-position: -140px -640px; }
-.emoji-motor_scooter { background-position: -160px -640px; }
-.emoji-motorboat { background-position: -180px -640px; }
-.emoji-motorcycle { background-position: -200px -640px; }
-.emoji-motorway { background-position: -220px -640px; }
-.emoji-mount_fuji { background-position: -240px -640px; }
-.emoji-mountain { background-position: -260px -640px; }
-.emoji-mountain_bicyclist { background-position: -280px -640px; }
-.emoji-mountain_bicyclist_tone1 { background-position: -300px -640px; }
-.emoji-mountain_bicyclist_tone2 { background-position: -320px -640px; }
-.emoji-mountain_bicyclist_tone3 { background-position: -340px -640px; }
-.emoji-mountain_bicyclist_tone4 { background-position: -360px -640px; }
-.emoji-mountain_bicyclist_tone5 { background-position: -380px -640px; }
-.emoji-mountain_cableway { background-position: -400px -640px; }
-.emoji-mountain_railway { background-position: -420px -640px; }
-.emoji-mountain_snow { background-position: -440px -640px; }
-.emoji-mouse { background-position: -460px -640px; }
-.emoji-mouse2 { background-position: -480px -640px; }
-.emoji-mouse_three_button { background-position: -500px -640px; }
-.emoji-movie_camera { background-position: -520px -640px; }
-.emoji-moyai { background-position: -540px -640px; }
-.emoji-mrs_claus { background-position: -560px -640px; }
-.emoji-mrs_claus_tone1 { background-position: -580px -640px; }
-.emoji-mrs_claus_tone2 { background-position: -600px -640px; }
-.emoji-mrs_claus_tone3 { background-position: -620px -640px; }
-.emoji-mrs_claus_tone4 { background-position: -640px -640px; }
-.emoji-mrs_claus_tone5 { background-position: -660px 0; }
-.emoji-muscle { background-position: -660px -20px; }
-.emoji-muscle_tone1 { background-position: -660px -40px; }
-.emoji-muscle_tone2 { background-position: -660px -60px; }
-.emoji-muscle_tone3 { background-position: -660px -80px; }
-.emoji-muscle_tone4 { background-position: -660px -100px; }
-.emoji-muscle_tone5 { background-position: -660px -120px; }
-.emoji-mushroom { background-position: -660px -140px; }
-.emoji-musical_keyboard { background-position: -660px -160px; }
-.emoji-musical_note { background-position: -660px -180px; }
-.emoji-musical_score { background-position: -660px -200px; }
-.emoji-mute { background-position: -660px -220px; }
-.emoji-nail_care { background-position: -660px -240px; }
-.emoji-nail_care_tone1 { background-position: -660px -260px; }
-.emoji-nail_care_tone2 { background-position: -660px -280px; }
-.emoji-nail_care_tone3 { background-position: -660px -300px; }
-.emoji-nail_care_tone4 { background-position: -660px -320px; }
-.emoji-nail_care_tone5 { background-position: -660px -340px; }
-.emoji-name_badge { background-position: -660px -360px; }
-.emoji-nauseated_face { background-position: -660px -380px; }
-.emoji-necktie { background-position: -660px -400px; }
-.emoji-negative_squared_cross_mark { background-position: -660px -420px; }
-.emoji-nerd { background-position: -660px -440px; }
-.emoji-neutral_face { background-position: -660px -460px; }
-.emoji-new { background-position: -660px -480px; }
-.emoji-new_moon { background-position: -660px -500px; }
-.emoji-new_moon_with_face { background-position: -660px -520px; }
-.emoji-newspaper { background-position: -660px -540px; }
-.emoji-newspaper2 { background-position: -660px -560px; }
-.emoji-ng { background-position: -660px -580px; }
-.emoji-night_with_stars { background-position: -660px -600px; }
-.emoji-nine { background-position: -660px -620px; }
-.emoji-no_bell { background-position: -660px -640px; }
-.emoji-no_bicycles { background-position: 0 -660px; }
-.emoji-no_entry { background-position: -20px -660px; }
-.emoji-no_entry_sign { background-position: -40px -660px; }
-.emoji-no_good { background-position: -60px -660px; }
-.emoji-no_good_tone1 { background-position: -80px -660px; }
-.emoji-no_good_tone2 { background-position: -100px -660px; }
-.emoji-no_good_tone3 { background-position: -120px -660px; }
-.emoji-no_good_tone4 { background-position: -140px -660px; }
-.emoji-no_good_tone5 { background-position: -160px -660px; }
-.emoji-no_mobile_phones { background-position: -180px -660px; }
-.emoji-no_mouth { background-position: -200px -660px; }
-.emoji-no_pedestrians { background-position: -220px -660px; }
-.emoji-no_smoking { background-position: -240px -660px; }
-.emoji-non-potable_water { background-position: -260px -660px; }
-.emoji-nose { background-position: -280px -660px; }
-.emoji-nose_tone1 { background-position: -300px -660px; }
-.emoji-nose_tone2 { background-position: -320px -660px; }
-.emoji-nose_tone3 { background-position: -340px -660px; }
-.emoji-nose_tone4 { background-position: -360px -660px; }
-.emoji-nose_tone5 { background-position: -380px -660px; }
-.emoji-notebook { background-position: -400px -660px; }
-.emoji-notebook_with_decorative_cover { background-position: -420px -660px; }
-.emoji-notepad_spiral { background-position: -440px -660px; }
-.emoji-notes { background-position: -460px -660px; }
-.emoji-nut_and_bolt { background-position: -480px -660px; }
-.emoji-o { background-position: -500px -660px; }
-.emoji-o2 { background-position: -520px -660px; }
-.emoji-ocean { background-position: -540px -660px; }
-.emoji-octagonal_sign { background-position: -560px -660px; }
-.emoji-octopus { background-position: -580px -660px; }
-.emoji-oden { background-position: -600px -660px; }
-.emoji-office { background-position: -620px -660px; }
-.emoji-oil { background-position: -640px -660px; }
-.emoji-ok { background-position: -660px -660px; }
-.emoji-ok_hand { background-position: -680px 0; }
-.emoji-ok_hand_tone1 { background-position: -680px -20px; }
-.emoji-ok_hand_tone2 { background-position: -680px -40px; }
-.emoji-ok_hand_tone3 { background-position: -680px -60px; }
-.emoji-ok_hand_tone4 { background-position: -680px -80px; }
-.emoji-ok_hand_tone5 { background-position: -680px -100px; }
-.emoji-ok_woman { background-position: -680px -120px; }
-.emoji-ok_woman_tone1 { background-position: -680px -140px; }
-.emoji-ok_woman_tone2 { background-position: -680px -160px; }
-.emoji-ok_woman_tone3 { background-position: -680px -180px; }
-.emoji-ok_woman_tone4 { background-position: -680px -200px; }
-.emoji-ok_woman_tone5 { background-position: -680px -220px; }
-.emoji-older_man { background-position: -680px -240px; }
-.emoji-older_man_tone1 { background-position: -680px -260px; }
-.emoji-older_man_tone2 { background-position: -680px -280px; }
-.emoji-older_man_tone3 { background-position: -680px -300px; }
-.emoji-older_man_tone4 { background-position: -680px -320px; }
-.emoji-older_man_tone5 { background-position: -680px -340px; }
-.emoji-older_woman { background-position: -680px -360px; }
-.emoji-older_woman_tone1 { background-position: -680px -380px; }
-.emoji-older_woman_tone2 { background-position: -680px -400px; }
-.emoji-older_woman_tone3 { background-position: -680px -420px; }
-.emoji-older_woman_tone4 { background-position: -680px -440px; }
-.emoji-older_woman_tone5 { background-position: -680px -460px; }
-.emoji-om_symbol { background-position: -680px -480px; }
-.emoji-on { background-position: -680px -500px; }
-.emoji-oncoming_automobile { background-position: -680px -520px; }
-.emoji-oncoming_bus { background-position: -680px -540px; }
-.emoji-oncoming_police_car { background-position: -680px -560px; }
-.emoji-oncoming_taxi { background-position: -680px -580px; }
-.emoji-one { background-position: -680px -600px; }
-.emoji-open_file_folder { background-position: -680px -620px; }
-.emoji-open_hands { background-position: -680px -640px; }
-.emoji-open_hands_tone1 { background-position: -680px -660px; }
-.emoji-open_hands_tone2 { background-position: 0 -680px; }
-.emoji-open_hands_tone3 { background-position: -20px -680px; }
-.emoji-open_hands_tone4 { background-position: -40px -680px; }
-.emoji-open_hands_tone5 { background-position: -60px -680px; }
-.emoji-open_mouth { background-position: -80px -680px; }
-.emoji-ophiuchus { background-position: -100px -680px; }
-.emoji-orange_book { background-position: -120px -680px; }
-.emoji-orthodox_cross { background-position: -140px -680px; }
-.emoji-outbox_tray { background-position: -160px -680px; }
-.emoji-owl { background-position: -180px -680px; }
-.emoji-ox { background-position: -200px -680px; }
-.emoji-package { background-position: -220px -680px; }
-.emoji-page_facing_up { background-position: -240px -680px; }
-.emoji-page_with_curl { background-position: -260px -680px; }
-.emoji-pager { background-position: -280px -680px; }
-.emoji-paintbrush { background-position: -300px -680px; }
-.emoji-palm_tree { background-position: -320px -680px; }
-.emoji-pancakes { background-position: -340px -680px; }
-.emoji-panda_face { background-position: -360px -680px; }
-.emoji-paperclip { background-position: -380px -680px; }
-.emoji-paperclips { background-position: -400px -680px; }
-.emoji-park { background-position: -420px -680px; }
-.emoji-parking { background-position: -440px -680px; }
-.emoji-part_alternation_mark { background-position: -460px -680px; }
-.emoji-partly_sunny { background-position: -480px -680px; }
-.emoji-passport_control { background-position: -500px -680px; }
-.emoji-pause_button { background-position: -520px -680px; }
-.emoji-peace { background-position: -540px -680px; }
-.emoji-peach { background-position: -560px -680px; }
-.emoji-peanuts { background-position: -580px -680px; }
-.emoji-pear { background-position: -600px -680px; }
-.emoji-pen_ballpoint { background-position: -620px -680px; }
-.emoji-pen_fountain { background-position: -640px -680px; }
-.emoji-pencil { background-position: -660px -680px; }
-.emoji-pencil2 { background-position: -680px -680px; }
-.emoji-penguin { background-position: -700px 0; }
-.emoji-pensive { background-position: -700px -20px; }
-.emoji-performing_arts { background-position: -700px -40px; }
-.emoji-persevere { background-position: -700px -60px; }
-.emoji-person_frowning { background-position: -700px -80px; }
-.emoji-person_frowning_tone1 { background-position: -700px -100px; }
-.emoji-person_frowning_tone2 { background-position: -700px -120px; }
-.emoji-person_frowning_tone3 { background-position: -700px -140px; }
-.emoji-person_frowning_tone4 { background-position: -700px -160px; }
-.emoji-person_frowning_tone5 { background-position: -700px -180px; }
-.emoji-person_with_blond_hair { background-position: -700px -200px; }
-.emoji-person_with_blond_hair_tone1 { background-position: -700px -220px; }
-.emoji-person_with_blond_hair_tone2 { background-position: -700px -240px; }
-.emoji-person_with_blond_hair_tone3 { background-position: -700px -260px; }
-.emoji-person_with_blond_hair_tone4 { background-position: -700px -280px; }
-.emoji-person_with_blond_hair_tone5 { background-position: -700px -300px; }
-.emoji-person_with_pouting_face { background-position: -700px -320px; }
-.emoji-person_with_pouting_face_tone1 { background-position: -700px -340px; }
-.emoji-person_with_pouting_face_tone2 { background-position: -700px -360px; }
-.emoji-person_with_pouting_face_tone3 { background-position: -700px -380px; }
-.emoji-person_with_pouting_face_tone4 { background-position: -700px -400px; }
-.emoji-person_with_pouting_face_tone5 { background-position: -700px -420px; }
-.emoji-pick { background-position: -700px -440px; }
-.emoji-pig { background-position: -700px -460px; }
-.emoji-pig2 { background-position: -700px -480px; }
-.emoji-pig_nose { background-position: -700px -500px; }
-.emoji-pill { background-position: -700px -520px; }
-.emoji-pineapple { background-position: -700px -540px; }
-.emoji-ping_pong { background-position: -700px -560px; }
-.emoji-pisces { background-position: -700px -580px; }
-.emoji-pizza { background-position: -700px -600px; }
-.emoji-place_of_worship { background-position: -700px -620px; }
-.emoji-play_pause { background-position: -700px -640px; }
-.emoji-point_down { background-position: -700px -660px; }
-.emoji-point_down_tone1 { background-position: -700px -680px; }
-.emoji-point_down_tone2 { background-position: 0 -700px; }
-.emoji-point_down_tone3 { background-position: -20px -700px; }
-.emoji-point_down_tone4 { background-position: -40px -700px; }
-.emoji-point_down_tone5 { background-position: -60px -700px; }
-.emoji-point_left { background-position: -80px -700px; }
-.emoji-point_left_tone1 { background-position: -100px -700px; }
-.emoji-point_left_tone2 { background-position: -120px -700px; }
-.emoji-point_left_tone3 { background-position: -140px -700px; }
-.emoji-point_left_tone4 { background-position: -160px -700px; }
-.emoji-point_left_tone5 { background-position: -180px -700px; }
-.emoji-point_right { background-position: -200px -700px; }
-.emoji-point_right_tone1 { background-position: -220px -700px; }
-.emoji-point_right_tone2 { background-position: -240px -700px; }
-.emoji-point_right_tone3 { background-position: -260px -700px; }
-.emoji-point_right_tone4 { background-position: -280px -700px; }
-.emoji-point_right_tone5 { background-position: -300px -700px; }
-.emoji-point_up { background-position: -320px -700px; }
-.emoji-point_up_2 { background-position: -340px -700px; }
-.emoji-point_up_2_tone1 { background-position: -360px -700px; }
-.emoji-point_up_2_tone2 { background-position: -380px -700px; }
-.emoji-point_up_2_tone3 { background-position: -400px -700px; }
-.emoji-point_up_2_tone4 { background-position: -420px -700px; }
-.emoji-point_up_2_tone5 { background-position: -440px -700px; }
-.emoji-point_up_tone1 { background-position: -460px -700px; }
-.emoji-point_up_tone2 { background-position: -480px -700px; }
-.emoji-point_up_tone3 { background-position: -500px -700px; }
-.emoji-point_up_tone4 { background-position: -520px -700px; }
-.emoji-point_up_tone5 { background-position: -540px -700px; }
-.emoji-police_car { background-position: -560px -700px; }
-.emoji-poodle { background-position: -580px -700px; }
-.emoji-poop { background-position: -600px -700px; }
-.emoji-popcorn { background-position: -620px -700px; }
-.emoji-post_office { background-position: -640px -700px; }
-.emoji-postal_horn { background-position: -660px -700px; }
-.emoji-postbox { background-position: -680px -700px; }
-.emoji-potable_water { background-position: -700px -700px; }
-.emoji-potato { background-position: -720px 0; }
-.emoji-pouch { background-position: -720px -20px; }
-.emoji-poultry_leg { background-position: -720px -40px; }
-.emoji-pound { background-position: -720px -60px; }
-.emoji-pouting_cat { background-position: -720px -80px; }
-.emoji-pray { background-position: -720px -100px; }
-.emoji-pray_tone1 { background-position: -720px -120px; }
-.emoji-pray_tone2 { background-position: -720px -140px; }
-.emoji-pray_tone3 { background-position: -720px -160px; }
-.emoji-pray_tone4 { background-position: -720px -180px; }
-.emoji-pray_tone5 { background-position: -720px -200px; }
-.emoji-prayer_beads { background-position: -720px -220px; }
-.emoji-pregnant_woman { background-position: -720px -240px; }
-.emoji-pregnant_woman_tone1 { background-position: -720px -260px; }
-.emoji-pregnant_woman_tone2 { background-position: -720px -280px; }
-.emoji-pregnant_woman_tone3 { background-position: -720px -300px; }
-.emoji-pregnant_woman_tone4 { background-position: -720px -320px; }
-.emoji-pregnant_woman_tone5 { background-position: -720px -340px; }
-.emoji-prince { background-position: -720px -360px; }
-.emoji-prince_tone1 { background-position: -720px -380px; }
-.emoji-prince_tone2 { background-position: -720px -400px; }
-.emoji-prince_tone3 { background-position: -720px -420px; }
-.emoji-prince_tone4 { background-position: -720px -440px; }
-.emoji-prince_tone5 { background-position: -720px -460px; }
-.emoji-princess { background-position: -720px -480px; }
-.emoji-princess_tone1 { background-position: -720px -500px; }
-.emoji-princess_tone2 { background-position: -720px -520px; }
-.emoji-princess_tone3 { background-position: -720px -540px; }
-.emoji-princess_tone4 { background-position: -720px -560px; }
-.emoji-princess_tone5 { background-position: -720px -580px; }
-.emoji-printer { background-position: -720px -600px; }
-.emoji-projector { background-position: -720px -620px; }
-.emoji-punch { background-position: -720px -640px; }
-.emoji-punch_tone1 { background-position: -720px -660px; }
-.emoji-punch_tone2 { background-position: -720px -680px; }
-.emoji-punch_tone3 { background-position: -720px -700px; }
-.emoji-punch_tone4 { background-position: 0 -720px; }
-.emoji-punch_tone5 { background-position: -20px -720px; }
-.emoji-purple_heart { background-position: -40px -720px; }
-.emoji-purse { background-position: -60px -720px; }
-.emoji-pushpin { background-position: -80px -720px; }
-.emoji-put_litter_in_its_place { background-position: -100px -720px; }
-.emoji-question { background-position: -120px -720px; }
-.emoji-rabbit { background-position: -140px -720px; }
-.emoji-rabbit2 { background-position: -160px -720px; }
-.emoji-race_car { background-position: -180px -720px; }
-.emoji-racehorse { background-position: -200px -720px; }
-.emoji-radio { background-position: -220px -720px; }
-.emoji-radio_button { background-position: -240px -720px; }
-.emoji-radioactive { background-position: -260px -720px; }
-.emoji-rage { background-position: -280px -720px; }
-.emoji-railway_car { background-position: -300px -720px; }
-.emoji-railway_track { background-position: -320px -720px; }
-.emoji-rainbow { background-position: -340px -720px; }
-.emoji-raised_back_of_hand { background-position: -360px -720px; }
-.emoji-raised_back_of_hand_tone1 { background-position: -380px -720px; }
-.emoji-raised_back_of_hand_tone2 { background-position: -400px -720px; }
-.emoji-raised_back_of_hand_tone3 { background-position: -420px -720px; }
-.emoji-raised_back_of_hand_tone4 { background-position: -440px -720px; }
-.emoji-raised_back_of_hand_tone5 { background-position: -460px -720px; }
-.emoji-raised_hand { background-position: -480px -720px; }
-.emoji-raised_hand_tone1 { background-position: -500px -720px; }
-.emoji-raised_hand_tone2 { background-position: -520px -720px; }
-.emoji-raised_hand_tone3 { background-position: -540px -720px; }
-.emoji-raised_hand_tone4 { background-position: -560px -720px; }
-.emoji-raised_hand_tone5 { background-position: -580px -720px; }
-.emoji-raised_hands { background-position: -600px -720px; }
-.emoji-raised_hands_tone1 { background-position: -620px -720px; }
-.emoji-raised_hands_tone2 { background-position: -640px -720px; }
-.emoji-raised_hands_tone3 { background-position: -660px -720px; }
-.emoji-raised_hands_tone4 { background-position: -680px -720px; }
-.emoji-raised_hands_tone5 { background-position: -700px -720px; }
-.emoji-raising_hand { background-position: -720px -720px; }
-.emoji-raising_hand_tone1 { background-position: -740px 0; }
-.emoji-raising_hand_tone2 { background-position: -740px -20px; }
-.emoji-raising_hand_tone3 { background-position: -740px -40px; }
-.emoji-raising_hand_tone4 { background-position: -740px -60px; }
-.emoji-raising_hand_tone5 { background-position: -740px -80px; }
-.emoji-ram { background-position: -740px -100px; }
-.emoji-ramen { background-position: -740px -120px; }
-.emoji-rat { background-position: -740px -140px; }
-.emoji-record_button { background-position: -740px -160px; }
-.emoji-recycle { background-position: -740px -180px; }
-.emoji-red_car { background-position: -740px -200px; }
-.emoji-red_circle { background-position: -740px -220px; }
-.emoji-registered { background-position: -740px -240px; }
-.emoji-relaxed { background-position: -740px -260px; }
-.emoji-relieved { background-position: -740px -280px; }
-.emoji-reminder_ribbon { background-position: -740px -300px; }
-.emoji-repeat { background-position: -740px -320px; }
-.emoji-repeat_one { background-position: -740px -340px; }
-.emoji-restroom { background-position: -740px -360px; }
-.emoji-revolving_hearts { background-position: -740px -380px; }
-.emoji-rewind { background-position: -740px -400px; }
-.emoji-rhino { background-position: -740px -420px; }
-.emoji-ribbon { background-position: -740px -440px; }
-.emoji-rice { background-position: -740px -460px; }
-.emoji-rice_ball { background-position: -740px -480px; }
-.emoji-rice_cracker { background-position: -740px -500px; }
-.emoji-rice_scene { background-position: -740px -520px; }
-.emoji-right_facing_fist { background-position: -740px -540px; }
-.emoji-right_facing_fist_tone1 { background-position: -740px -560px; }
-.emoji-right_facing_fist_tone2 { background-position: -740px -580px; }
-.emoji-right_facing_fist_tone3 { background-position: -740px -600px; }
-.emoji-right_facing_fist_tone4 { background-position: -740px -620px; }
-.emoji-right_facing_fist_tone5 { background-position: -740px -640px; }
-.emoji-ring { background-position: -740px -660px; }
-.emoji-robot { background-position: -740px -680px; }
-.emoji-rocket { background-position: -740px -700px; }
-.emoji-rofl { background-position: -740px -720px; }
-.emoji-roller_coaster { background-position: 0 -740px; }
-.emoji-rolling_eyes { background-position: -20px -740px; }
-.emoji-rooster { background-position: -40px -740px; }
-.emoji-rose { background-position: -60px -740px; }
-.emoji-rosette { background-position: -80px -740px; }
-.emoji-rotating_light { background-position: -100px -740px; }
-.emoji-round_pushpin { background-position: -120px -740px; }
-.emoji-rowboat { background-position: -140px -740px; }
-.emoji-rowboat_tone1 { background-position: -160px -740px; }
-.emoji-rowboat_tone2 { background-position: -180px -740px; }
-.emoji-rowboat_tone3 { background-position: -200px -740px; }
-.emoji-rowboat_tone4 { background-position: -220px -740px; }
-.emoji-rowboat_tone5 { background-position: -240px -740px; }
-.emoji-rugby_football { background-position: -260px -740px; }
-.emoji-runner { background-position: -280px -740px; }
-.emoji-runner_tone1 { background-position: -300px -740px; }
-.emoji-runner_tone2 { background-position: -320px -740px; }
-.emoji-runner_tone3 { background-position: -340px -740px; }
-.emoji-runner_tone4 { background-position: -360px -740px; }
-.emoji-runner_tone5 { background-position: -380px -740px; }
-.emoji-running_shirt_with_sash { background-position: -400px -740px; }
-.emoji-sa { background-position: -420px -740px; }
-.emoji-sagittarius { background-position: -440px -740px; }
-.emoji-sailboat { background-position: -460px -740px; }
-.emoji-sake { background-position: -480px -740px; }
-.emoji-salad { background-position: -500px -740px; }
-.emoji-sandal { background-position: -520px -740px; }
-.emoji-santa { background-position: -540px -740px; }
-.emoji-santa_tone1 { background-position: -560px -740px; }
-.emoji-santa_tone2 { background-position: -580px -740px; }
-.emoji-santa_tone3 { background-position: -600px -740px; }
-.emoji-santa_tone4 { background-position: -620px -740px; }
-.emoji-santa_tone5 { background-position: -640px -740px; }
-.emoji-satellite { background-position: -660px -740px; }
-.emoji-satellite_orbital { background-position: -680px -740px; }
-.emoji-saxophone { background-position: -700px -740px; }
-.emoji-scales { background-position: -720px -740px; }
-.emoji-school { background-position: -740px -740px; }
-.emoji-school_satchel { background-position: -760px 0; }
-.emoji-scissors { background-position: -760px -20px; }
-.emoji-scooter { background-position: -760px -40px; }
-.emoji-scorpion { background-position: -760px -60px; }
-.emoji-scorpius { background-position: -760px -80px; }
-.emoji-scream { background-position: -760px -100px; }
-.emoji-scream_cat { background-position: -760px -120px; }
-.emoji-scroll { background-position: -760px -140px; }
-.emoji-seat { background-position: -760px -160px; }
-.emoji-second_place { background-position: -760px -180px; }
-.emoji-secret { background-position: -760px -200px; }
-.emoji-see_no_evil { background-position: -760px -220px; }
-.emoji-seedling { background-position: -760px -240px; }
-.emoji-selfie { background-position: -760px -260px; }
-.emoji-selfie_tone1 { background-position: -760px -280px; }
-.emoji-selfie_tone2 { background-position: -760px -300px; }
-.emoji-selfie_tone3 { background-position: -760px -320px; }
-.emoji-selfie_tone4 { background-position: -760px -340px; }
-.emoji-selfie_tone5 { background-position: -760px -360px; }
-.emoji-seven { background-position: -760px -380px; }
-.emoji-shallow_pan_of_food { background-position: -760px -400px; }
-.emoji-shamrock { background-position: -760px -420px; }
-.emoji-shark { background-position: -760px -440px; }
-.emoji-shaved_ice { background-position: -760px -460px; }
-.emoji-sheep { background-position: -760px -480px; }
-.emoji-shell { background-position: -760px -500px; }
-.emoji-shield { background-position: -760px -520px; }
-.emoji-shinto_shrine { background-position: -760px -540px; }
-.emoji-ship { background-position: -760px -560px; }
-.emoji-shirt { background-position: -760px -580px; }
-.emoji-shopping_bags { background-position: -760px -600px; }
-.emoji-shopping_cart { background-position: -760px -620px; }
-.emoji-shower { background-position: -760px -640px; }
-.emoji-shrimp { background-position: -760px -660px; }
-.emoji-shrug { background-position: -760px -680px; }
-.emoji-shrug_tone1 { background-position: -760px -700px; }
-.emoji-shrug_tone2 { background-position: -760px -720px; }
-.emoji-shrug_tone3 { background-position: -760px -740px; }
-.emoji-shrug_tone4 { background-position: 0 -760px; }
-.emoji-shrug_tone5 { background-position: -20px -760px; }
-.emoji-signal_strength { background-position: -40px -760px; }
-.emoji-six { background-position: -60px -760px; }
-.emoji-six_pointed_star { background-position: -80px -760px; }
-.emoji-ski { background-position: -100px -760px; }
-.emoji-skier { background-position: -120px -760px; }
-.emoji-skull { background-position: -140px -760px; }
-.emoji-skull_crossbones { background-position: -160px -760px; }
-.emoji-sleeping { background-position: -180px -760px; }
-.emoji-sleeping_accommodation { background-position: -200px -760px; }
-.emoji-sleepy { background-position: -220px -760px; }
-.emoji-slight_frown { background-position: -240px -760px; }
-.emoji-slight_smile { background-position: -260px -760px; }
-.emoji-slot_machine { background-position: -280px -760px; }
-.emoji-small_blue_diamond { background-position: -300px -760px; }
-.emoji-small_orange_diamond { background-position: -320px -760px; }
-.emoji-small_red_triangle { background-position: -340px -760px; }
-.emoji-small_red_triangle_down { background-position: -360px -760px; }
-.emoji-smile { background-position: -380px -760px; }
-.emoji-smile_cat { background-position: -400px -760px; }
-.emoji-smiley { background-position: -420px -760px; }
-.emoji-smiley_cat { background-position: -440px -760px; }
-.emoji-smiling_imp { background-position: -460px -760px; }
-.emoji-smirk { background-position: -480px -760px; }
-.emoji-smirk_cat { background-position: -500px -760px; }
-.emoji-smoking { background-position: -520px -760px; }
-.emoji-snail { background-position: -540px -760px; }
-.emoji-snake { background-position: -560px -760px; }
-.emoji-sneezing_face { background-position: -580px -760px; }
-.emoji-snowboarder { background-position: -600px -760px; }
-.emoji-snowflake { background-position: -620px -760px; }
-.emoji-snowman { background-position: -640px -760px; }
-.emoji-snowman2 { background-position: -660px -760px; }
-.emoji-sob { background-position: -680px -760px; }
-.emoji-soccer { background-position: -700px -760px; }
-.emoji-soon { background-position: -720px -760px; }
-.emoji-sos { background-position: -740px -760px; }
-.emoji-sound { background-position: -760px -760px; }
-.emoji-space_invader { background-position: -780px 0; }
-.emoji-spades { background-position: -780px -20px; }
-.emoji-spaghetti { background-position: -780px -40px; }
-.emoji-sparkle { background-position: -780px -60px; }
-.emoji-sparkler { background-position: -780px -80px; }
-.emoji-sparkles { background-position: -780px -100px; }
-.emoji-sparkling_heart { background-position: -780px -120px; }
-.emoji-speak_no_evil { background-position: -780px -140px; }
-.emoji-speaker { background-position: -780px -160px; }
-.emoji-speaking_head { background-position: -780px -180px; }
-.emoji-speech_balloon { background-position: -780px -200px; }
-.emoji-speech_left { background-position: -780px -220px; }
-.emoji-speedboat { background-position: -780px -240px; }
-.emoji-spider { background-position: -780px -260px; }
-.emoji-spider_web { background-position: -780px -280px; }
-.emoji-spoon { background-position: -780px -300px; }
-.emoji-spy { background-position: -780px -320px; }
-.emoji-spy_tone1 { background-position: -780px -340px; }
-.emoji-spy_tone2 { background-position: -780px -360px; }
-.emoji-spy_tone3 { background-position: -780px -380px; }
-.emoji-spy_tone4 { background-position: -780px -400px; }
-.emoji-spy_tone5 { background-position: -780px -420px; }
-.emoji-squid { background-position: -780px -440px; }
-.emoji-stadium { background-position: -780px -460px; }
-.emoji-star { background-position: -780px -480px; }
-.emoji-star2 { background-position: -780px -500px; }
-.emoji-star_and_crescent { background-position: -780px -520px; }
-.emoji-star_of_david { background-position: -780px -540px; }
-.emoji-stars { background-position: -780px -560px; }
-.emoji-station { background-position: -780px -580px; }
-.emoji-statue_of_liberty { background-position: -780px -600px; }
-.emoji-steam_locomotive { background-position: -780px -620px; }
-.emoji-stew { background-position: -780px -640px; }
-.emoji-stop_button { background-position: -780px -660px; }
-.emoji-stopwatch { background-position: -780px -680px; }
-.emoji-straight_ruler { background-position: -780px -700px; }
-.emoji-strawberry { background-position: -780px -720px; }
-.emoji-stuck_out_tongue { background-position: -780px -740px; }
-.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -760px; }
-.emoji-stuck_out_tongue_winking_eye { background-position: 0 -780px; }
-.emoji-stuffed_flatbread { background-position: -20px -780px; }
-.emoji-sun_with_face { background-position: -40px -780px; }
-.emoji-sunflower { background-position: -60px -780px; }
-.emoji-sunglasses { background-position: -80px -780px; }
-.emoji-sunny { background-position: -100px -780px; }
-.emoji-sunrise { background-position: -120px -780px; }
-.emoji-sunrise_over_mountains { background-position: -140px -780px; }
-.emoji-surfer { background-position: -160px -780px; }
-.emoji-surfer_tone1 { background-position: -180px -780px; }
-.emoji-surfer_tone2 { background-position: -200px -780px; }
-.emoji-surfer_tone3 { background-position: -220px -780px; }
-.emoji-surfer_tone4 { background-position: -240px -780px; }
-.emoji-surfer_tone5 { background-position: -260px -780px; }
-.emoji-sushi { background-position: -280px -780px; }
-.emoji-suspension_railway { background-position: -300px -780px; }
-.emoji-sweat { background-position: -320px -780px; }
-.emoji-sweat_drops { background-position: -340px -780px; }
-.emoji-sweat_smile { background-position: -360px -780px; }
-.emoji-sweet_potato { background-position: -380px -780px; }
-.emoji-swimmer { background-position: -400px -780px; }
-.emoji-swimmer_tone1 { background-position: -420px -780px; }
-.emoji-swimmer_tone2 { background-position: -440px -780px; }
-.emoji-swimmer_tone3 { background-position: -460px -780px; }
-.emoji-swimmer_tone4 { background-position: -480px -780px; }
-.emoji-swimmer_tone5 { background-position: -500px -780px; }
-.emoji-symbols { background-position: -520px -780px; }
-.emoji-synagogue { background-position: -540px -780px; }
-.emoji-syringe { background-position: -560px -780px; }
-.emoji-taco { background-position: -580px -780px; }
-.emoji-tada { background-position: -600px -780px; }
-.emoji-tanabata_tree { background-position: -620px -780px; }
-.emoji-tangerine { background-position: -640px -780px; }
-.emoji-taurus { background-position: -660px -780px; }
-.emoji-taxi { background-position: -680px -780px; }
-.emoji-tea { background-position: -700px -780px; }
-.emoji-telephone { background-position: -720px -780px; }
-.emoji-telephone_receiver { background-position: -740px -780px; }
-.emoji-telescope { background-position: -760px -780px; }
-.emoji-ten { background-position: -780px -780px; }
-.emoji-tennis { background-position: -800px 0; }
-.emoji-tent { background-position: -800px -20px; }
-.emoji-thermometer { background-position: -800px -40px; }
-.emoji-thermometer_face { background-position: -800px -60px; }
-.emoji-thinking { background-position: -800px -80px; }
-.emoji-third_place { background-position: -800px -100px; }
-.emoji-thought_balloon { background-position: -800px -120px; }
-.emoji-three { background-position: -800px -140px; }
-.emoji-thumbsdown { background-position: -800px -160px; }
-.emoji-thumbsdown_tone1 { background-position: -800px -180px; }
-.emoji-thumbsdown_tone2 { background-position: -800px -200px; }
-.emoji-thumbsdown_tone3 { background-position: -800px -220px; }
-.emoji-thumbsdown_tone4 { background-position: -800px -240px; }
-.emoji-thumbsdown_tone5 { background-position: -800px -260px; }
-.emoji-thumbsup { background-position: -800px -280px; }
-.emoji-thumbsup_tone1 { background-position: -800px -300px; }
-.emoji-thumbsup_tone2 { background-position: -800px -320px; }
-.emoji-thumbsup_tone3 { background-position: -800px -340px; }
-.emoji-thumbsup_tone4 { background-position: -800px -360px; }
-.emoji-thumbsup_tone5 { background-position: -800px -380px; }
-.emoji-thunder_cloud_rain { background-position: -800px -400px; }
-.emoji-ticket { background-position: -800px -420px; }
-.emoji-tickets { background-position: -800px -440px; }
-.emoji-tiger { background-position: -800px -460px; }
-.emoji-tiger2 { background-position: -800px -480px; }
-.emoji-timer { background-position: -800px -500px; }
-.emoji-tired_face { background-position: -800px -520px; }
-.emoji-tm { background-position: -800px -540px; }
-.emoji-toilet { background-position: -800px -560px; }
-.emoji-tokyo_tower { background-position: -800px -580px; }
-.emoji-tomato { background-position: -800px -600px; }
-.emoji-tone1 { background-position: -800px -620px; }
-.emoji-tone2 { background-position: -800px -640px; }
-.emoji-tone3 { background-position: -800px -660px; }
-.emoji-tone4 { background-position: -800px -680px; }
-.emoji-tone5 { background-position: -800px -700px; }
-.emoji-tongue { background-position: -800px -720px; }
-.emoji-tools { background-position: -800px -740px; }
-.emoji-top { background-position: -800px -760px; }
-.emoji-tophat { background-position: -800px -780px; }
-.emoji-track_next { background-position: 0 -800px; }
-.emoji-track_previous { background-position: -20px -800px; }
-.emoji-trackball { background-position: -40px -800px; }
-.emoji-tractor { background-position: -60px -800px; }
-.emoji-traffic_light { background-position: -80px -800px; }
-.emoji-train { background-position: -100px -800px; }
-.emoji-train2 { background-position: -120px -800px; }
-.emoji-tram { background-position: -140px -800px; }
-.emoji-triangular_flag_on_post { background-position: -160px -800px; }
-.emoji-triangular_ruler { background-position: -180px -800px; }
-.emoji-trident { background-position: -200px -800px; }
-.emoji-triumph { background-position: -220px -800px; }
-.emoji-trolleybus { background-position: -240px -800px; }
-.emoji-trophy { background-position: -260px -800px; }
-.emoji-tropical_drink { background-position: -280px -800px; }
-.emoji-tropical_fish { background-position: -300px -800px; }
-.emoji-truck { background-position: -320px -800px; }
-.emoji-trumpet { background-position: -340px -800px; }
-.emoji-tulip { background-position: -360px -800px; }
-.emoji-tumbler_glass { background-position: -380px -800px; }
-.emoji-turkey { background-position: -400px -800px; }
-.emoji-turtle { background-position: -420px -800px; }
-.emoji-tv { background-position: -440px -800px; }
-.emoji-twisted_rightwards_arrows { background-position: -460px -800px; }
-.emoji-two { background-position: -480px -800px; }
-.emoji-two_hearts { background-position: -500px -800px; }
-.emoji-two_men_holding_hands { background-position: -520px -800px; }
-.emoji-two_women_holding_hands { background-position: -540px -800px; }
-.emoji-u5272 { background-position: -560px -800px; }
-.emoji-u5408 { background-position: -580px -800px; }
-.emoji-u55b6 { background-position: -600px -800px; }
-.emoji-u6307 { background-position: -620px -800px; }
-.emoji-u6708 { background-position: -640px -800px; }
-.emoji-u6709 { background-position: -660px -800px; }
-.emoji-u6e80 { background-position: -680px -800px; }
-.emoji-u7121 { background-position: -700px -800px; }
-.emoji-u7533 { background-position: -720px -800px; }
-.emoji-u7981 { background-position: -740px -800px; }
-.emoji-u7a7a { background-position: -760px -800px; }
-.emoji-umbrella { background-position: -780px -800px; }
-.emoji-umbrella2 { background-position: -800px -800px; }
-.emoji-unamused { background-position: -820px 0; }
-.emoji-underage { background-position: -820px -20px; }
-.emoji-unicorn { background-position: -820px -40px; }
-.emoji-unlock { background-position: -820px -60px; }
-.emoji-up { background-position: -820px -80px; }
-.emoji-upside_down { background-position: -820px -100px; }
-.emoji-urn { background-position: -820px -120px; }
-.emoji-v { background-position: -820px -140px; }
-.emoji-v_tone1 { background-position: -820px -160px; }
-.emoji-v_tone2 { background-position: -820px -180px; }
-.emoji-v_tone3 { background-position: -820px -200px; }
-.emoji-v_tone4 { background-position: -820px -220px; }
-.emoji-v_tone5 { background-position: -820px -240px; }
-.emoji-vertical_traffic_light { background-position: -820px -260px; }
-.emoji-vhs { background-position: -820px -280px; }
-.emoji-vibration_mode { background-position: -820px -300px; }
-.emoji-video_camera { background-position: -820px -320px; }
-.emoji-video_game { background-position: -820px -340px; }
-.emoji-violin { background-position: -820px -360px; }
-.emoji-virgo { background-position: -820px -380px; }
-.emoji-volcano { background-position: -820px -400px; }
-.emoji-volleyball { background-position: -820px -420px; }
-.emoji-vs { background-position: -820px -440px; }
-.emoji-vulcan { background-position: -820px -460px; }
-.emoji-vulcan_tone1 { background-position: -820px -480px; }
-.emoji-vulcan_tone2 { background-position: -820px -500px; }
-.emoji-vulcan_tone3 { background-position: -820px -520px; }
-.emoji-vulcan_tone4 { background-position: -820px -540px; }
-.emoji-vulcan_tone5 { background-position: -820px -560px; }
-.emoji-walking { background-position: -820px -580px; }
-.emoji-walking_tone1 { background-position: -820px -600px; }
-.emoji-walking_tone2 { background-position: -820px -620px; }
-.emoji-walking_tone3 { background-position: -820px -640px; }
-.emoji-walking_tone4 { background-position: -820px -660px; }
-.emoji-walking_tone5 { background-position: -820px -680px; }
-.emoji-waning_crescent_moon { background-position: -820px -700px; }
-.emoji-waning_gibbous_moon { background-position: -820px -720px; }
-.emoji-warning { background-position: -820px -740px; }
-.emoji-wastebasket { background-position: -820px -760px; }
-.emoji-watch { background-position: -820px -780px; }
-.emoji-water_buffalo { background-position: -820px -800px; }
-.emoji-water_polo { background-position: 0 -820px; }
-.emoji-water_polo_tone1 { background-position: -20px -820px; }
-.emoji-water_polo_tone2 { background-position: -40px -820px; }
-.emoji-water_polo_tone3 { background-position: -60px -820px; }
-.emoji-water_polo_tone4 { background-position: -80px -820px; }
-.emoji-water_polo_tone5 { background-position: -100px -820px; }
-.emoji-watermelon { background-position: -120px -820px; }
-.emoji-wave { background-position: -140px -820px; }
-.emoji-wave_tone1 { background-position: -160px -820px; }
-.emoji-wave_tone2 { background-position: -180px -820px; }
-.emoji-wave_tone3 { background-position: -200px -820px; }
-.emoji-wave_tone4 { background-position: -220px -820px; }
-.emoji-wave_tone5 { background-position: -240px -820px; }
-.emoji-wavy_dash { background-position: -260px -820px; }
-.emoji-waxing_crescent_moon { background-position: -280px -820px; }
-.emoji-waxing_gibbous_moon { background-position: -300px -820px; }
-.emoji-wc { background-position: -320px -820px; }
-.emoji-weary { background-position: -340px -820px; }
-.emoji-wedding { background-position: -360px -820px; }
-.emoji-whale { background-position: -380px -820px; }
-.emoji-whale2 { background-position: -400px -820px; }
-.emoji-wheel_of_dharma { background-position: -420px -820px; }
-.emoji-wheelchair { background-position: -440px -820px; }
-.emoji-white_check_mark { background-position: -460px -820px; }
-.emoji-white_circle { background-position: -480px -820px; }
-.emoji-white_flower { background-position: -500px -820px; }
-.emoji-white_large_square { background-position: -520px -820px; }
-.emoji-white_medium_small_square { background-position: -540px -820px; }
-.emoji-white_medium_square { background-position: -560px -820px; }
-.emoji-white_small_square { background-position: -580px -820px; }
-.emoji-white_square_button { background-position: -600px -820px; }
-.emoji-white_sun_cloud { background-position: -620px -820px; }
-.emoji-white_sun_rain_cloud { background-position: -640px -820px; }
-.emoji-white_sun_small_cloud { background-position: -660px -820px; }
-.emoji-wilted_rose { background-position: -680px -820px; }
-.emoji-wind_blowing_face { background-position: -700px -820px; }
-.emoji-wind_chime { background-position: -720px -820px; }
-.emoji-wine_glass { background-position: -740px -820px; }
-.emoji-wink { background-position: -760px -820px; }
-.emoji-wolf { background-position: -780px -820px; }
-.emoji-woman { background-position: -800px -820px; }
-.emoji-woman_tone1 { background-position: -820px -820px; }
-.emoji-woman_tone2 { background-position: -840px 0; }
-.emoji-woman_tone3 { background-position: -840px -20px; }
-.emoji-woman_tone4 { background-position: -840px -40px; }
-.emoji-woman_tone5 { background-position: -840px -60px; }
-.emoji-womans_clothes { background-position: -840px -80px; }
-.emoji-womans_hat { background-position: -840px -100px; }
-.emoji-womens { background-position: -840px -120px; }
-.emoji-worried { background-position: -840px -140px; }
-.emoji-wrench { background-position: -840px -160px; }
-.emoji-wrestlers { background-position: -840px -180px; }
-.emoji-wrestlers_tone1 { background-position: -840px -200px; }
-.emoji-wrestlers_tone2 { background-position: -840px -220px; }
-.emoji-wrestlers_tone3 { background-position: -840px -240px; }
-.emoji-wrestlers_tone4 { background-position: -840px -260px; }
-.emoji-wrestlers_tone5 { background-position: -840px -280px; }
-.emoji-writing_hand { background-position: -840px -300px; }
-.emoji-writing_hand_tone1 { background-position: -840px -320px; }
-.emoji-writing_hand_tone2 { background-position: -840px -340px; }
-.emoji-writing_hand_tone3 { background-position: -840px -360px; }
-.emoji-writing_hand_tone4 { background-position: -840px -380px; }
-.emoji-writing_hand_tone5 { background-position: -840px -400px; }
-.emoji-x { background-position: -840px -420px; }
-.emoji-yellow_heart { background-position: -840px -440px; }
-.emoji-yen { background-position: -840px -460px; }
-.emoji-yin_yang { background-position: -840px -480px; }
-.emoji-yum { background-position: -840px -500px; }
-.emoji-zap { background-position: -840px -520px; }
-.emoji-zero { background-position: -840px -540px; }
-.emoji-zipper_mouth { background-position: -840px -560px; }
-.emoji-100 { background-position: -840px -580px; }
-
-.emoji-icon {
- background-image: image-url('emoji.png');
- background-repeat: no-repeat;
- color: transparent;
- text-indent: -99em;
- height: 20px;
- width: 20px;
-
- @media only screen and (-webkit-min-device-pixel-ratio: 2),
- only screen and (min--moz-device-pixel-ratio: 2),
- only screen and (-o-min-device-pixel-ratio: 2/1),
- only screen and (min-device-pixel-ratio: 2),
- only screen and (min-resolution: 192dpi),
- only screen and (min-resolution: 2dppx) {
- background-image: image-url('emoji@2x.png');
- background-size: 860px 840px;
- }
-}
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
index 4f26cd015e4..cad915bc86f 100644
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -79,7 +79,7 @@
border-right-color: $dropdown-border-color;
}
- .popover-content {
+ .popover-body {
padding: 0;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index d835d49d8b2..f77ec4b6a2c 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -75,7 +75,7 @@
padding: 8px $gl-padding;
border-bottom: 1px solid $border-color;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: left;
}
@@ -125,7 +125,7 @@
&.wiki {
padding: $gl-padding;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
padding: $gl-padding * 2;
}
}
@@ -364,7 +364,7 @@ span.idiff {
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
.file-actions {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 621a4adc0cb..0ee5748952a 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -9,19 +9,19 @@
float: right;
margin-right: 0;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
float: none;
}
}
}
.filters-section {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: inline-block;
}
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.filter-item:not(:last-child) {
margin-right: 6px;
}
@@ -37,7 +37,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.filter-item {
display: block;
margin: 0 0 10px;
@@ -53,7 +53,7 @@
display: -webkit-flex;
display: flex;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
-webkit-flex-direction: column;
flex-direction: column;
}
@@ -193,7 +193,7 @@
border: 1px solid $border-color;
background-color: $white-light;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
-webkit-flex: 1 1 auto;
flex: 1 1 auto;
margin-bottom: 10px;
@@ -264,7 +264,7 @@
max-width: 280px;
overflow: auto;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
width: auto;
left: 0;
right: 0;
@@ -315,7 +315,7 @@
.filtered-search-history-dropdown {
width: 40%;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
left: 0;
right: 0;
max-width: none;
@@ -353,7 +353,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.issues-details-filters {
padding: 0 0 10px;
background-color: $white-light;
@@ -361,7 +361,7 @@
}
}
-@media (max-width: $screen-xs) {
+@include media-breakpoint-down(sm) {
.filter-dropdown-container {
.dropdown-toggle,
.dropdown,
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index cb2f71b0033..52c3f18a682 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -8,19 +8,19 @@
.flash-notice {
@extend .alert;
- @extend .alert-info;
+ background-color: $blue-500;
margin: 0;
}
.flash-warning {
@extend .alert;
- @extend .alert-warning;
+ background-color: $orange-500;
margin: 0;
}
.flash-alert {
@extend .alert;
- @extend .alert-danger;
+ background-color: $red-500;
margin: 0;
.flash-text,
@@ -42,14 +42,16 @@
.flash-success {
@extend .alert;
- @extend .alert-success;
+ background-color: $green-500;
margin: 0;
}
.flash-notice,
.flash-alert,
- .flash-success {
+ .flash-success,
+ .flash-warning {
border-radius: $border-radius-default;
+ color: $white-light;
.container-fluid,
.container-fluid.container-limited {
@@ -72,7 +74,7 @@
}
}
-@media (max-width: $screen-sm-max) {
+@include media-breakpoint-down(sm) {
ul.notes {
.flash-container.timeline-content {
margin-left: 0;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 2c30311b1c1..c76ea532912 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -36,8 +36,8 @@ label {
}
}
-.control-label {
- @extend .col-sm-2;
+.form-control-label {
+ @extend .col-md-2;
}
.inline-input-group {
@@ -48,21 +48,21 @@ label {
width: 150px;
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.custom-form-control {
width: 150px;
}
}
/* Medium devices (desktops, 992px and up) */
-@media (min-width: $screen-md-min) {
+@include media-breakpoint-up(md) {
.custom-form-control {
width: 170px;
}
}
/* Large devices (large desktops, 1200px and up) */
-@media (min-width: $screen-lg-min) {
+@include media-breakpoint-up(lg) {
.custom-form-control {
width: 200px;
}
@@ -72,7 +72,7 @@ label {
margin-left: 0;
margin-right: 0;
- .control-label {
+ .form-control-label {
font-weight: $gl-font-weight-bold;
padding-top: 4px;
}
@@ -83,7 +83,8 @@ label {
font-family: $monospace_font;
}
- .input-group-btn .btn {
+ .input-group-prepend .btn,
+ .input-group-append .btn {
padding: 3px $gl-btn-padding;
background-color: $gray-light;
border: 1px solid $border-color;
@@ -102,10 +103,10 @@ label {
}
}
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 0 $gl-padding;
- .control-label,
+ .form-control-label,
.text-block {
padding-left: 0;
}
@@ -120,6 +121,14 @@ label {
@include box-shadow(none);
border-radius: 2px;
padding: $gl-vert-padding $gl-input-padding;
+
+ &.input-short {
+ width: $input-short-width;
+
+ @include media-breakpoint-up(md) {
+ width: $input-short-md-width;
+ }
+ }
}
.select-wrapper {
@@ -155,8 +164,8 @@ label {
margin-top: 35px;
}
-.form-group .control-label,
-.form-group .control-label-full-width {
+.form-group .form-control-label,
+.form-group .form-control-label-full-width {
font-weight: $gl-font-weight-normal;
}
@@ -170,17 +179,19 @@ label {
max-width: 180px;
}
- .input-group-addon {
+ .input-group-prepend,
+ .input-group-append {
background-color: $input-group-addon-bg;
}
- .input-group-addon:not(:first-child):not(:last-child) {
+ .input-group-prepend:not(:first-child):not(:last-child),
+ .input-group-append:not(:first-child):not(:last-child) {
border-left: 0;
border-right: 0;
}
}
-.help-block {
+.form-text.text-muted {
margin-bottom: 0;
margin-top: #{$grid-size / 2};
}
@@ -221,7 +232,7 @@ label {
}
}
-@media(max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.remember-me {
.remember-me-checkbox {
margin-top: 0;
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 05cb0196ced..d6ae8cbb416 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -21,7 +21,7 @@
}
.container-fluid {
- .navbar-toggle {
+ .navbar-toggler {
border-left: 1px solid lighten($color-700, 10%);
}
}
@@ -63,7 +63,7 @@
&:hover,
&:focus {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
background-color: rgba($color-200, 0.2);
}
@@ -165,37 +165,18 @@
}
}
- .sidebar-top-level-items > li.active .badge {
+ .sidebar-top-level-items > li.active .badge.badge-pill {
color: $color-800;
}
- .nav-links li.active a {
- border-bottom-color: $color-500;
+ .nav-links li a.active {
+ border-bottom: 2px solid $color-500;
- .badge {
+ .badge.badge-pill {
font-weight: $gl-font-weight-bold;
}
}
- // Web IDE
- .ide-sidebar-link {
- color: $color-200;
- background-color: $color-700;
-
- &:hover,
- &:focus {
- background-color: $color-500;
- }
-
- &:active {
- background: $color-800;
- }
- }
-
- .branch-container {
- border-left-color: $color-700;
- }
-
.branch-header-title {
color: $color-700;
}
@@ -203,6 +184,13 @@
.ide-file-list .file.file-active {
color: $color-700;
}
+
+ .ide-sidebar-link {
+ &.active {
+ color: $color-700;
+ box-shadow: inset 3px 0 $color-700;
+ }
+ }
}
body {
@@ -289,8 +277,8 @@ body {
}
.container-fluid {
- .navbar-toggle,
- .navbar-toggle:hover {
+ .navbar-toggler,
+ .navbar-toggler:hover {
color: $theme-gray-700;
border-left: 1px solid $theme-gray-200;
}
@@ -340,12 +328,8 @@ body {
}
}
- .sidebar-top-level-items > li.active .badge {
+ .sidebar-top-level-items > li.active .badge.badge-pill {
color: $theme-gray-900;
}
-
- .ide-sidebar-link {
- color: $white-light;
- }
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 0136af76a13..2085e5646ef 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -37,6 +37,7 @@
}
.header-content {
+ width: 100%;
display: -webkit-flex;
display: flex;
justify-content: space-between;
@@ -91,7 +92,7 @@
border-radius: $border-radius-default;
.tanuki-logo {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-right: 8px;
}
}
@@ -110,7 +111,7 @@
}
&.menu-expanded {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.title-container {
display: none;
}
@@ -133,13 +134,13 @@
border-top: 0;
padding: 0;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex: 1 1 auto;
}
.nav {
- > li:not(.hidden-xs) a {
- @media (max-width: $screen-xs-max) {
+ > li:not(.d-none) a {
+ @include media-breakpoint-down(xs) {
margin-left: 0;
min-width: 100%;
}
@@ -156,7 +157,7 @@
}
}
- .navbar-toggle {
+ .navbar-toggler {
right: -10px;
border-radius: 0;
min-width: 45px;
@@ -181,14 +182,14 @@
}
.navbar-nav {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: -webkit-flex;
display: flex;
padding-right: 10px;
}
li {
- .badge {
+ .badge.badge-pill {
box-shadow: none;
font-weight: $gl-font-weight-bold;
}
@@ -197,7 +198,7 @@
.nav > li {
&.header-user {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding-left: 10px;
}
}
@@ -208,7 +209,7 @@
padding: 6px 8px;
height: 32px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 0;
}
@@ -324,8 +325,8 @@
fill: currentColor;
}
-.header-user .dropdown-menu-nav,
-.header-new .dropdown-menu-nav {
+.header-user .dropdown-menu,
+.header-new .dropdown-menu {
margin-top: $dropdown-vertical-offset;
}
@@ -378,7 +379,7 @@
margin-bottom: 0;
line-height: 16px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-wrap: wrap;
}
@@ -410,7 +411,7 @@
.breadcrumb-item-text {
text-decoration: inherit;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
@include str-truncated(128px);
}
}
@@ -452,7 +453,7 @@
.navbar-nav {
li {
- .badge {
+ .badge.badge-pill {
position: inherit;
font-weight: $gl-font-weight-normal;
margin-left: -6px;
@@ -478,7 +479,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.navbar-gitlab .container-fluid {
font-size: 18px;
@@ -493,7 +494,7 @@
margin-left: -8px;
margin-right: -10px;
- .nav > li:not(.hidden-xs) {
+ .nav > li:not(.d-none) {
display: table-cell !important;
width: 25%;
@@ -514,7 +515,7 @@
}
.header-user {
- .dropdown-menu-nav {
+ .dropdown-menu {
width: auto;
min-width: 160px;
margin-top: 4px;
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 62a0fba3da3..ab3cceceae9 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -39,35 +39,10 @@
svg {
fill: currentColor;
- &.s8 {
- @include svg-size(8px);
- }
-
- &.s12 {
- @include svg-size(12px);
- }
-
- &.s16 {
- @include svg-size(16px);
- }
-
- &.s18 {
- @include svg-size(18px);
- }
-
- &.s24 {
- @include svg-size(24px);
- }
-
- &.s32 {
- @include svg-size(32px);
- }
-
- &.s48 {
- @include svg-size(48px);
- }
-
- &.s72 {
- @include svg-size(72px);
+ $svg-sizes: 8 12 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/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index d8c57a0e2d9..1d247671761 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -11,7 +11,7 @@
padding: 5px 11px;
margin-top: 4px;
/* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding: 0 $gl-btn-padding;
margin-top: 5px;
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index d107422e517..0536c39cee7 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -15,7 +15,7 @@ body {
background-color: $white-light !important;
}
- &.card-content {
+ &.board-card-content {
background-color: $gray-darker;
.content-wrapper {
@@ -74,7 +74,7 @@ body {
}
/* Center alert text and alert action links on smaller screens */
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
.alert {
text-align: center;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index f1a8a46dda4..17f4958d535 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -2,7 +2,7 @@
* Well styled list
*
*/
-.well-list {
+.card-body-list {
position: relative;
margin: 0;
padding: 0;
@@ -72,7 +72,7 @@
}
}
- .well-title {
+ .card.card-body-title {
font-size: $list-font-size;
line-height: 18px;
}
@@ -159,7 +159,7 @@ ul.content-list {
&:last-child {
margin-right: 0;
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin: 0 auto;
}
}
@@ -173,7 +173,7 @@ ul.content-list {
.member-controls {
float: none;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: right;
}
}
@@ -228,7 +228,7 @@ ul.content-list {
}
}
- .label-default {
+ .badge-secondary {
color: $gl-text-color-secondary;
}
@@ -237,7 +237,7 @@ ul.content-list {
}
}
-.panel > .content-list > li {
+.card > .content-list > li {
padding: $gl-padding-top $gl-padding;
}
@@ -279,251 +279,14 @@ ul.indent-list {
padding: 10px 0 0 30px;
}
-
// Specific styles for tree list
@keyframes spin-avatar {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
-.groups-list-tree-container {
- .has-no-search-results {
- text-align: center;
- padding: $gl-padding;
- font-style: italic;
- color: $well-light-text-color;
- }
-
- > .group-list-tree > .group-row.has-children:first-child {
- border-top: 0;
- }
-}
-
-.group-list-tree {
- .avatar-container.content-loading {
- position: relative;
-
- > a,
- > a .avatar {
- height: 100%;
- border-radius: 50%;
- }
-
- > a {
- padding: 2px;
-
- .avatar {
- border: 2px solid $white-normal;
-
- &.identicon {
- line-height: 15px;
- }
- }
- }
-
- &::after {
- content: "";
- position: absolute;
- height: 100%;
- width: 100%;
- background-color: transparent;
- border: 2px outset $kdb-border;
- border-radius: 50%;
- animation: spin-avatar 3s infinite linear;
- }
- }
-
- .folder-toggle-wrap {
- float: left;
- line-height: $list-text-height;
- font-size: 0;
-
- span {
- font-size: $gl-font-size;
- }
- }
-
- .folder-caret,
- .item-type-icon {
- display: inline-block;
- }
-
- .folder-caret {
- width: 15px;
-
- svg {
- margin-bottom: 2px;
- }
- }
-
- .item-type-icon {
- margin-top: 2px;
- width: 20px;
- }
-
- > .group-row:not(.has-children) {
- .folder-caret {
- opacity: 0;
- }
- }
-
- .content-list li:last-child {
- padding-bottom: 0;
- }
-
- .group-list-tree {
- margin-bottom: 0;
- margin-left: 30px;
- position: relative;
-
- &::before {
- content: '';
- display: block;
- width: 0;
- position: absolute;
- top: 5px;
- bottom: 0;
- left: -16px;
- border-left: 2px solid $border-white-normal;
- }
-
- .group-row {
- position: relative;
-
- &::before {
- content: "";
- display: block;
- width: 10px;
- height: 0;
- border-top: 2px solid $border-white-normal;
- position: absolute;
- top: 30px;
- left: -16px;
- }
-
- &:last-child::before {
- background: $white-light;
- height: auto;
- top: 30px;
- bottom: 0;
- }
-
- &.being-removed {
- opacity: 0.5;
- }
- }
- }
-
- .group-row {
- padding: 0;
-
- &.has-children {
- border-top: 0;
- }
-
- &:first-child {
- border-top: 1px solid $white-normal;
- }
-
- &:last-of-type {
- .group-row-contents:not(:hover) {
- border-bottom: 1px solid transparent;
- }
- }
- }
-
- .group-row-contents {
- padding: 10px 10px 8px;
- border-top: solid 1px transparent;
- border-bottom: solid 1px $white-normal;
-
- &:hover {
- border-color: $row-hover-border;
- background-color: $row-hover;
- cursor: pointer;
- }
-
- .avatar-container > a {
- width: 100%;
- text-decoration: none;
- }
-
- &.has-more-items {
- display: block;
- padding: 20px 10px;
- }
-
- .stats {
- position: relative;
- line-height: 46px;
-
- > span {
- display: inline-flex;
- align-items: center;
- height: 16px;
- min-width: 30px;
- }
-
- > span:last-child {
- margin-right: 0;
- }
-
- .stat-value {
- margin: 2px 0 0 5px;
- }
- }
-
- .controls {
- margin-left: 5px;
-
- > .btn {
- margin-right: $btn-xs-side-margin;
- }
- }
- }
-
- .project-row-contents .stats {
- line-height: inherit;
-
- > span:first-child {
- margin-left: 25px;
- }
-
- .item-visibility {
- margin-right: 0;
- }
-
- .last-updated {
- position: absolute;
- right: 12px;
- min-width: 250px;
- text-align: right;
- color: $gl-text-color-secondary;
- }
- }
-}
-
.namespace-title {
.tooltip-inner {
max-width: 350px;
}
}
-
-ul.group-list-tree {
- li.group-row {
- > .group-row-contents .title {
- line-height: $list-text-height;
- }
-
- &.has-description > .group-row-contents .title {
- line-height: inherit;
- }
- }
-}
-
-.js-groups-list-holder {
- .groups-list-loading {
- font-size: 34px;
- text-align: center;
- }
-}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 938f5f49c09..b893151e4fe 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -74,7 +74,7 @@
}
.md-header-tab {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex: 1;
width: 100%;
border-bottom: 1px solid $border-color;
@@ -90,7 +90,7 @@
&.active {
display: block;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex: none;
display: flex;
justify-content: center;
@@ -107,6 +107,16 @@
padding-top: 10px;
}
+.referenced-commands {
+ background: $blue-50;
+ padding: $gl-padding-8 $gl-padding;
+ border-radius: $border-radius-default;
+
+ p {
+ margin: 0;
+ }
+}
+
.md-preview-holder {
min-height: 167px;
padding: 10px 0;
@@ -182,7 +192,7 @@
margin-left: $gl-padding;
margin-right: -5px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
margin-left: 0;
margin-right: 0;
}
@@ -258,7 +268,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.atwho-view-ul {
width: 350px;
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index e12b5aab381..0ea0b65b95f 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -17,6 +17,16 @@
*/
@mixin markdown-table {
width: auto;
+ display: inline-block;
+ overflow-x: auto;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 0;
+
+ @supports(width: fit-content) {
+ display: block;
+ width: fit-content;
+ }
}
/*
@@ -200,3 +210,15 @@
margin-left: -$size;
}
}
+
+/*
+ * Mixin that fixes wrapping issues with long strings (e.g. URLs)
+ *
+ * Note: the width needs to be set for it to work in Firefox
+ */
+@mixin overflow-break-word {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ max-width: 100%;
+}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 8604e753c18..6244fb86fea 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -1,5 +1,5 @@
/** Common mobile (screen XS, SM) styles **/
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.container .content {
margin-top: 20px;
}
@@ -14,7 +14,7 @@
font-size: 12px;
margin-right: 3px;
- .badge {
+ .badge.badge-pill {
display: none;
}
}
@@ -40,10 +40,6 @@
.project-home-panel {
padding-left: 0 !important;
- .project-avatar {
- display: block;
- }
-
.project-repo-buttons,
.git-clone-holder {
display: none;
@@ -90,7 +86,7 @@
}
}
-@media (max-width: $screen-sm-max) {
+@include media-breakpoint-down(sm) {
.issues-filters {
.milestone-filter {
display: none;
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index eb789cc64b0..667661d8b5c 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -46,7 +46,7 @@
margin-left: $grid-size;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-direction: column;
.btn + .btn {
@@ -55,7 +55,7 @@
}
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
.btn:first-of-type {
margin-left: auto;
}
@@ -74,7 +74,7 @@ body.modal-open {
}
}
-@media (min-width: $screen-lg-min) {
+@include media-breakpoint-up(lg) {
.modal-full {
width: 98%;
}
@@ -84,7 +84,7 @@ body.modal-open {
background-color: $black-transparent;
z-index: 2100;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
.modal-dialog {
margin: 30px auto;
}
diff --git a/app/assets/stylesheets/framework/page_header.scss b/app/assets/stylesheets/framework/page_header.scss
index 0c879f40930..660e3dcac8d 100644
--- a/app/assets/stylesheets/framework/page_header.scss
+++ b/app/assets/stylesheets/framework/page_header.scss
@@ -3,7 +3,7 @@
padding: 10px 0;
margin-bottom: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: flex;
align-items: center;
@@ -19,7 +19,7 @@
margin-right: 3px;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.btn {
width: 100%;
margin-top: 10px;
@@ -35,7 +35,7 @@
@extend .avatar-inline;
margin-left: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-left: 4px;
}
}
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index c3ec9db0f07..d3e013590b6 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -6,6 +6,7 @@
.pagination {
padding: 0;
+ margin: 20px 0;
a {
cursor: pointer;
@@ -30,7 +31,7 @@
}
}
-.panel > .gl-pagination {
+.card > .gl-pagination {
margin: 0;
}
@@ -44,7 +45,7 @@
display: none;
}
- .page {
+ .page-item {
display: none;
&.active {
@@ -57,13 +58,13 @@
/**
* Small screen pagination
*/
-@media (max-width: $screen-xs-min) {
+@include media-breakpoint-down(xs) {
.gl-pagination {
.pagination li a {
padding: 6px 10px;
}
- .page {
+ .page-item {
display: none;
&.active {
@@ -76,9 +77,9 @@
/**
* Medium screen pagination
*/
-@media (min-width: $screen-xs-min) and (max-width: $screen-md-max) {
+@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) {
.gl-pagination {
- .page {
+ .page-item {
display: none;
&.active,
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index e8d69e62194..a8e28104a94 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -1,14 +1,14 @@
-.panel {
+.card {
margin-bottom: $gl-padding;
}
-.panel-slim {
- @extend .panel;
+.card-slim {
+ @extend .card;
margin-bottom: $gl-vert-padding;
}
-.panel-heading {
+.card-header {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
@@ -21,7 +21,7 @@
line-height: 20px;
}
- .badge {
+ .badge.badge-pill {
margin-top: -2px;
margin-left: 5px;
}
@@ -41,12 +41,13 @@
}
}
-.panel-empty-heading {
+.card-empty-heading {
border-bottom: 0;
}
-.panel-body {
+.card-body {
padding: $gl-padding;
+ background-color: $white-light;
.form-actions {
margin: -$gl-padding;
@@ -54,7 +55,7 @@
}
}
-.panel-title {
+.card-title {
font-size: inherit;
line-height: inherit;
}
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 34fccf6f0a4..764bebd82c6 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -6,7 +6,7 @@
.gl-responsive-table-row-layout {
width: 100%;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
display: flex;
align-items: center;
@@ -21,7 +21,7 @@
margin-top: 10px;
border: 1px solid $border-color;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
margin: 0;
padding: $gl-padding 0;
border: 0;
@@ -44,13 +44,13 @@
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
max-width: #{$width + '%'};
}
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: flex;
align-self: stretch;
padding: 10px;
@@ -65,7 +65,7 @@
&.section-wrap {
white-space: normal;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
flex-wrap: wrap;
}
}
@@ -76,11 +76,11 @@
}
.table-button-footer {
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
text-align: right;
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: block;
align-self: stretch;
min-height: 0;
@@ -122,7 +122,7 @@
.table-row-header {
font-size: 13px;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: none;
}
}
@@ -132,13 +132,13 @@
color: $gl-text-color-secondary;
text-align: left;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
display: none;
}
}
.table-mobile-content {
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
@include flex-max-width(60);
text-align: right;
}
@@ -154,7 +154,7 @@
overflow: hidden;
text-overflow: ellipsis;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
flex: 0 0 90%;
}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 17c31d6b184..c3c64adf3da 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -27,18 +27,18 @@
color: $black;
border-bottom: 2px solid $gray-darkest;
- .badge {
+ .badge.badge-pill {
color: $black;
}
}
- }
-
- &.active a {
- color: $black;
- font-weight: $gl-font-weight-bold;
- .badge {
+ &.active {
color: $black;
+ font-weight: $gl-font-weight-bold;
+
+ .badge.badge-pill {
+ color: $black;
+ }
}
}
}
@@ -56,7 +56,7 @@
white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
}
@@ -80,7 +80,7 @@
}
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
&.mobile-separator {
@@ -124,21 +124,13 @@
position: relative;
/* Medium devices (desktops, 992px and up) */
- @media (min-width: $screen-md-min) { width: 200px; }
+ @include media-breakpoint-up(md) { width: 200px; }
/* Large devices (large desktops, 1200px and up) */
- @media (min-width: $screen-lg-min) { width: 250px; }
-
- &.input-short {
- /* Medium devices (desktops, 992px and up) */
- @media (min-width: $screen-md-min) { width: 170px; }
-
- /* Large devices (large desktops, 1200px and up) */
- @media (min-width: $screen-lg-min) { width: 210px; }
- }
+ @include media-breakpoint-up(lg) { width: 250px; }
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding-bottom: 0;
width: 100%;
@@ -164,10 +156,6 @@
}
}
- .input-short {
- width: 100%;
- }
-
.icon-label {
display: inline-block;
}
@@ -184,7 +172,7 @@
.nav-controls {
width: auto;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
}
@@ -204,7 +192,7 @@
width: 100%;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-flow: row wrap;
.nav-controls {
@@ -241,8 +229,6 @@
}
.scrolling-tabs-container {
- position: relative;
-
.merge-request-tabs-container & {
overflow: hidden;
}
@@ -272,8 +258,6 @@
}
.inner-page-scroll-tabs {
- position: relative;
-
.fade-right {
@include fade(left, $white-light);
right: 0;
@@ -368,7 +352,7 @@
max-width: 300px;
width: auto;
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 250px;
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 798f248dad4..8c716400913 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -16,7 +16,7 @@
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
- transition-duration: .3s;
+ transition-duration: 0.3s;
position: absolute;
top: 0;
cursor: pointer;
@@ -31,7 +31,7 @@
.right-sidebar-collapsed {
padding-right: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
@@ -65,13 +65,13 @@
display: inline-flex;
}
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ @include media-breakpoint-only(sm) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
.content-wrapper {
padding-right: $gutter_width;
}
@@ -137,6 +137,12 @@
}
}
+.issuable-sidebar .labels {
+ .value.dont-hide ~ .selectbox {
+ padding-top: $gl-padding-8;
+ }
+}
+
.pikaday-container {
.pika-single {
margin-top: 2px;
@@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
-
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 606d4675f19..f80e9b1014b 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -40,7 +40,7 @@
}
.snippet-actions {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: right;
}
}
@@ -67,7 +67,8 @@
padding: 8px 40px;
}
- .embed-toggle {
+ .embed-toggle,
+ .snippet-clipboard-btn {
height: 35px;
}
}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 5bde96caf42..a10bd1544c5 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -48,7 +48,7 @@ table {
}
.responsive-table {
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
th {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/tabs.scss b/app/assets/stylesheets/framework/tabs.scss
index c8ba14b7066..6b60149fbbb 100644
--- a/app/assets/stylesheets/framework/tabs.scss
+++ b/app/assets/stylesheets/framework/tabs.scss
@@ -1,6 +1,7 @@
.gitlab-tabs {
background: $gray-light;
border: 1px solid $border-color;
+ flex-wrap: nowrap;
li {
width: 50%;
diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss
new file mode 100644
index 00000000000..744fd0ff796
--- /dev/null
+++ b/app/assets/stylesheets/framework/terms.scss
@@ -0,0 +1,64 @@
+.terms {
+ .with-performance-bar & {
+ margin-top: 0;
+ }
+
+ .alert-wrapper {
+ min-height: $header-height + $gl-padding;
+ }
+
+ .content {
+ padding-top: $gl-padding;
+ }
+
+ .panel {
+ .panel-heading {
+ display: -webkit-flex;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ line-height: $line-height-base;
+
+ .title {
+ display: flex;
+ align-items: center;
+
+ .logo-text {
+ width: 55px;
+ height: 24px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+ }
+
+ .navbar-collapse {
+ padding-right: 0;
+
+ .navbar-nav {
+ margin: 0;
+ }
+ }
+
+ .nav li {
+ float: none;
+ }
+ }
+
+ .panel-content {
+ padding: $gl-padding;
+
+ *:first-child {
+ margin-top: 0;
+ }
+
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .footer-block {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 373f35e71d8..75c11590547 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -4,7 +4,7 @@
padding: 0;
&::before {
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, xs)) {
background: none;
}
}
@@ -34,7 +34,7 @@
.timeline-entry-inner {
position: relative;
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, xs)) {
.timeline-icon {
display: none;
}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 0cd83df218f..d5cc78a6680 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -121,7 +121,7 @@
cursor: not-allowed;
}
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
width: 50px;
&::before,
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
deleted file mode 100644
index 1c6e2bf3074..00000000000
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Twitter bootstrap with GitLab customizations/additions
- *
- */
-
-// Core variables and mixins
-@import "bootstrap/variables";
-@import "bootstrap/mixins";
-
-// Reset
-@import "bootstrap/normalize";
-@import "bootstrap/print";
-
-// Core CSS
-@import "bootstrap/scaffolding";
-@import "bootstrap/type";
-@import "bootstrap/code";
-@import "bootstrap/grid";
-@import "bootstrap/tables";
-@import "bootstrap/forms";
-@import "bootstrap/buttons";
-
-// Components
-@import "bootstrap/component-animations";
-// @import "bootstrap/dropdowns";
-@import "bootstrap/button-groups";
-@import "bootstrap/input-groups";
-@import "bootstrap/navs";
-@import "bootstrap/navbar";
-@import "bootstrap/breadcrumbs";
-@import "bootstrap/pagination";
-@import "bootstrap/pager";
-@import "bootstrap/labels";
-@import "bootstrap/badges";
-@import "bootstrap/alerts";
-@import "bootstrap/progress-bars";
-@import "bootstrap/list-group";
-@import "bootstrap/wells";
-@import "bootstrap/close";
-@import "bootstrap/panels";
-
-// Components w/ JavaScript
-@import "bootstrap/modals";
-@import "bootstrap/tooltip";
-@import "bootstrap/popovers";
-
-// Utility classes
-.clearfix {
- @include clearfix();
-}
-
-.center-block {
- @include center-block();
-}
-
-.pull-right {
- float: right !important;
-}
-
-.pull-left {
- float: left !important;
-}
-
-.hide {
- display: none;
-}
-
-.show {
- display: block !important;
-}
-
-.invisible {
- visibility: hidden;
-}
-
-.text-hide {
- @include text-hide();
-}
-
-.hidden {
- display: none !important;
- visibility: hidden !important;
-}
-
-.affix {
- position: fixed;
-}
-
-/*
- * Fix <summary> elements on firefox
- * See https://github.com/necolas/normalize.css/issues/640
- * and https://github.com/twbs/bootstrap/issues/21060
- *
- */
-summary {
- display: list-item;
-}
-
-@import "bootstrap/responsive-utilities";
-
-// Labels
-.label {
- padding: 4px 5px;
- font-size: 12px;
- font-style: normal;
- font-weight: $gl-font-weight-normal;
- display: inline-block;
-
- &.label-gray {
- background-color: $label-gray-bg;
- color: $gl-text-color;
- text-shadow: none;
- }
-
- &.label-inverse {
- background-color: $label-inverse-bg;
- }
-}
-
-
-/**
- * fix to keep tooltips position in top navigation bar
- *
- */
-.navbar .nav > li {
- position: relative;
- white-space: nowrap;
-}
-
-/**
- * Add some extra stuff to panels
- *
- */
-
-.panel {
- box-shadow: none;
-
- .panel-body {
- form,
- pre {
- margin: 0;
- }
-
- .form-actions {
- margin: -15px;
- margin-top: 18px;
- }
- }
-
- .panel-footer {
- .pagination {
- margin: 0;
- }
-
- .btn {
- min-width: 124px;
- }
-
- .btn-clipboard {
- min-width: 0;
- }
- }
-
- &.panel-small {
- .panel-heading {
- padding: 6px 15px;
- font-size: 13px;
- font-weight: $gl-font-weight-normal;
-
- a {
- color: $panel-heading-link-color;
- }
- }
- }
-
- &.panel-without-border {
- border: 0;
- }
-
- &.panel-without-margin {
- margin: 0;
- }
-}
-
-.panel-succes .panel-heading,
-.panel-info .panel-heading,
-.panel-danger .panel-heading,
-.panel-warning .panel-heading,
-.panel-primary .panel-heading,
-.alert {
- a:not(.btn) {
- @extend .alert-link;
- color: $white-light;
- text-decoration: underline;
- }
-}
-
-// Prevent datetimes on tooltips to break into two lines
-.local-timeago {
- white-space: nowrap;
-}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
deleted file mode 100644
index d04e555769b..00000000000
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ /dev/null
@@ -1,199 +0,0 @@
-// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3):
-// For all variables see https://github.com/twbs/bootstrap-sass/blob/master/templates/project/_bootstrap-variables.sass
-//
-// Variables
-// --------------------------------------------------
-
-
-//== Colors
-//
-//## Gray and brand colors for use across Bootstrap.
-
-// $gray-base: #000
-// $gray-darker: lighten($gray-base, 13.5%) // #222
-// $gray-dark: lighten($gray-base, 20%) // #333
-// $gray: lighten($gray-base, 33.5%) // #555
-// $gray-light: lighten($gray-base, 46.7%) // #777
-// $gray-lighter: lighten($gray-base, 93.5%) // #eee
-
-$brand-primary: $gl-primary;
-$brand-success: $gl-success;
-$brand-info: $gl-info;
-$brand-warning: $gl-warning;
-$brand-danger: $gl-danger;
-
-$border-radius-base: 3px !default;
-$border-radius-large: 3px !default;
-$border-radius-small: 3px !default;
-
-
-//== Scaffolding
-//
-$text-color: $gl-text-color;
-$link-color: $gl-link-color;
-$link-hover-color: $gl-link-hover-color;
-
-
-//== Typography
-//
-//## Font, line-height, and color for body text, headings, and more.
-
-$font-family-sans-serif: $regular_font;
-$font-family-monospace: $monospace_font;
-$font-size-base: $gl-font-size;
-
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-$padding-base-vertical: $gl-vert-padding;
-$padding-base-horizontal: $gl-padding;
-$component-active-color: $white-light;
-$component-active-bg: $brand-info;
-
-//== Forms
-//
-//##
-
-$input-color: $text-color;
-$input-border: $border-color;
-$input-border-focus: $focus-border-color;
-$legend-color: $text-color;
-
-
-//== Pagination
-//
-//##
-
-$pagination-color: $gl-text-color;
-$pagination-bg: $white-light;
-$pagination-border: $border-color;
-
-$pagination-hover-color: $gl-text-color;
-$pagination-hover-bg: $row-hover;
-$pagination-hover-border: $border-color;
-
-$pagination-active-color: $white-light;
-$pagination-active-bg: $gl-link-color;
-$pagination-active-border: $gl-link-color;
-
-$pagination-disabled-color: #cdcdcd;
-$pagination-disabled-bg: $gray-light;
-$pagination-disabled-border: $border-color;
-
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-$state-success-text: $white-light;
-$state-success-bg: $brand-success;
-$state-success-border: $brand-success;
-
-$state-info-text: $white-light;
-$state-info-bg: $brand-info;
-$state-info-border: $brand-info;
-
-$state-warning-text: $white-light;
-$state-warning-bg: $brand-warning;
-$state-warning-border: $brand-warning;
-
-$state-danger-text: $white-light;
-$state-danger-bg: $brand-danger;
-$state-danger-border: $brand-danger;
-
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-$alert-border-radius: 0;
-
-
-//== Panels
-//
-//##
-
-$panel-border-radius: 2px;
-$panel-default-text: $text-color;
-$panel-default-border: $border-color;
-$panel-default-heading-bg: $gray-light;
-$panel-footer-bg: $gray-light;
-$panel-inner-border: $border-color;
-
-$badge-bg: $badge-bg;
-$badge-color: $badge-color;
-
-//== Wells
-//
-//##
-
-$well-bg: $gray-light;
-$well-border: #eee;
-
-//== Code
-//
-//##
-
-$code-color: $red-600;
-$code-bg: lighten($red-100, 2%);
-
-$kbd-color: $white-light;
-$kbd-bg: #333;
-
-//== Buttons
-//
-//##
-$btn-default-color: $gl-text-color;
-$btn-default-bg: $white-light;
-$btn-default-border: #e7e9ed;
-
-//== Nav
-//
-//##
-$nav-link-padding: 13px $gl-padding;
-
-//== Code
-//
-//##
-$pre-bg: $gray-light !default;
-$pre-color: $gl-text-color !default;
-$pre-border-color: $border-color;
-
-$table-bg-accent: $gray-light;
-
-$zindex-popover: 900;
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-$modal-inner-padding: $gl-padding;
-
-//** Padding applied to the modal title
-$modal-title-padding: $gl-padding;
-//** Modal title line-height
-// $modal-title-line-height: $line-height-base
-
-//** Background color of modal content area
-$modal-content-bg: $gray-light;
-$modal-body-bg: $white-light;
-//** Modal content border color
-// $modal-content-border-color: rgba(0,0,0,.2)
-//** Modal content border color **for IE8**
-// $modal-content-fallback-border-color: #999
-
-//** Modal backdrop background color
-// $modal-backdrop-bg: #000
-//** Modal backdrop opacity
-// $modal-backdrop-opacity: .5
-//** Modal header border color
-// $modal-header-border-color: #e5e5e5
-//** Modal footer border color
-// $modal-footer-border-color: $modal-header-border-color
-
-$modal-lg: 860px;
-$modal-md: 540px;
-// $modal-sm: 300px
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9e1371648ed..ed0bfbbe08b 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -114,7 +114,7 @@
font-size: 0.95em;
}
- blockquote {
+ .blockquote {
color: $gl-grayish-blue;
font-size: inherit;
padding: 8px 24px;
@@ -122,12 +122,12 @@
border-left: 3px solid $white-dark;
}
- blockquote:dir(rtl) {
+ .blockquote:dir(rtl) {
border-left: 0;
border-right: 3px solid $white-dark;
}
- blockquote p {
+ .blockquote p {
color: $gl-grayish-blue !important;
font-size: inherit;
line-height: 1.5;
@@ -321,6 +321,16 @@ h6 {
/** CODE **/
pre {
font-family: $monospace_font;
+ display: block;
+ padding: $gl-padding-8;
+ margin: 0 0 $gl-padding-8;
+ font-size: 13px;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: $gl-text-color;
+ background-color: $gray-light;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-small;
}
code {
@@ -391,7 +401,7 @@ h4 {
}
.text-right-lg {
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
text-align: right;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 37223175199..89b61530ddb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -142,6 +142,11 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
/*
+ * Override Bootstrap 4 variables
+ */
+$secondary: $gray-light;
+
+/*
* UI elements
*/
$border-color: #e5e5e5;
@@ -212,6 +217,7 @@ $tooltip-font-size: 12px;
/*
* Padding
*/
+$gl-padding-24: 24px;
$gl-padding: 16px;
$gl-padding-8: 8px;
$gl-padding-4: 4px;
@@ -229,6 +235,7 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 40px;
+$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
@@ -247,6 +254,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
+$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
@@ -262,6 +270,7 @@ $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$breadcrumb-min-height: 48px;
+$gcp-signup-offer-icon-max-width: 125px;
/*
* Common component specific colors
@@ -302,6 +311,11 @@ $gl-warning: $orange-500;
$gl-danger: $red-500;
$gl-btn-active-background: rgba(0, 0, 0, 0.16);
$gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
+// Bootstrap override states
+$success: $gl-success;
+$info: $gl-info;
+$warning: $gl-warning;
+$danger: $gl-danger;
/*
* Commit Diff Colors
@@ -331,11 +345,10 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
-$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas',
- 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
- Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif,
- 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
+ 'Courier New', 'andale mono', 'lucida console', monospace;
+$regular_font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
/*
* Dropdowns
@@ -373,6 +386,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08);
+$sidebar-toggle-height: 60px;
+$sidebar-milestone-toggle-bottom-margin: 10px;
/*
* Buttons
@@ -461,11 +476,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
*/
$issue-boards-filter-height: 68px;
$issue-boards-breadcrumbs-height-xs: 63px;
-$issue-board-list-difference-xs: $header-height +
- $issue-boards-breadcrumbs-height-xs;
+$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
-$issue-board-list-difference-md: $issue-board-list-difference-sm +
- $issue-boards-filter-height;
+$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
/*
* Avatar
@@ -551,6 +564,8 @@ $input-danger-border: $red-400;
$input-group-addon-bg: #f7f8fa;
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
$gl-field-focus-shadow-error: rgba($red-500, 0.6);
+$input-short-width: 200px;
+$input-short-md-width: 280px;
/*
* Help
@@ -686,6 +701,8 @@ $stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px;
$pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
+$ci-action-dropdown-button-size: 24px;
+$ci-action-dropdown-svg-size: 12px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index 2f3a80daa90..514fac82b1e 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -5,7 +5,7 @@
border-radius: $border-radius-default;
margin-bottom: $gl-padding;
- .well-segment {
+ .card.card-body-segment {
padding: $gl-padding;
&:not(:last-of-type) {
@@ -19,6 +19,7 @@
.fork-svg {
margin-right: 4px;
+ vertical-align: bottom;
}
}
@@ -58,7 +59,7 @@
}
}
- .label.label-gray {
+ .label-gray {
background-color: $well-expand-item;
}
@@ -107,7 +108,7 @@
}
}
-.well-centered {
+.card.card-body-centered {
h1 {
font-weight: $gl-font-weight-normal;
text-align: center;
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 658ac26fca9..b5eda79e5ed 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -138,7 +138,7 @@ pre {
margin: 0;
}
-blockquote {
+.blockquote {
color: $gl-grayish-blue;
padding: 0 0 0 15px;
margin: 0;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 318d3ddaece..1c3d312f7ac 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -1,5 +1,3 @@
-@import './issues/issue_count_badge';
-
[v-cloak] {
display: none;
}
@@ -60,7 +58,7 @@
.boards-app {
position: relative;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
transition: width $sidebar-transition-duration;
width: 100%;
@@ -83,11 +81,11 @@
white-space: nowrap;
min-height: 200px;
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ @include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
height: calc(100vh - #{$issue-board-list-difference-md});
}
@@ -96,13 +94,13 @@
100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
);
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ @include media-breakpoint-only(sm) {
height: calc(
100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
);
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
height: calc(
100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
);
@@ -119,7 +117,7 @@
white-space: normal;
vertical-align: top;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 400px;
}
@@ -276,7 +274,7 @@
font-size: (26px / $issue-boards-font-size) * 1em;
}
-.card {
+.board-card {
position: relative;
padding: 11px 10px 11px $gl-padding;
background: $white-light;
@@ -284,12 +282,15 @@
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
+ // as a fallback, hide overflow content so that dragging and dropping still works
+ overflow: hidden;
+
&:not(:last-child) {
margin-bottom: 5px;
}
&.is-active,
- &.is-active .card-assignee:hover a {
+ &.is-active .board-card-assignee:hover a {
background-color: $row-hover;
&:first-child:not(:only-child) {
@@ -297,7 +298,7 @@
}
}
- .label {
+ .badge {
border: 0;
outline: 0;
}
@@ -309,23 +310,23 @@
}
}
-.card-title {
+.board-card-title {
+ @include overflow-break-word();
margin: 0 30px 0 0;
font-size: 1em;
line-height: inherit;
a {
color: $gl-text-color;
- word-wrap: break-word;
margin-right: 2px;
}
}
-.card-header {
+.board-card-header {
display: flex;
min-height: 20px;
- .card-assignee {
+ .board-card-assignee {
display: flex;
justify-content: flex-end;
position: absolute;
@@ -396,16 +397,16 @@
}
}
-.card-footer {
+.board-card-footer {
margin: 0 0 5px;
- .label {
+ .badge {
margin-top: 5px;
margin-right: 6px;
}
}
-.card-number {
+.board-card-number {
font-size: 12px;
color: $gl-text-color-secondary;
}
@@ -461,6 +462,7 @@
}
.issuable-header-text {
+ @include overflow-break-word();
padding-right: 35px;
> strong {
@@ -562,11 +564,11 @@
.add-issues-list-column {
width: 100%;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 50%;
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: (100% / 3);
}
}
@@ -581,11 +583,11 @@
margin-right: -$gl-vert-padding;
overflow-y: scroll;
- .card-parent {
+ .board-card-parent {
padding: 0 5px 5px;
}
- .card {
+ .board-card {
border: 1px solid $border-gray-dark;
box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, 0.3);
cursor: pointer;
@@ -635,7 +637,7 @@
display: none;
margin-right: 10px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: block;
}
}
@@ -643,7 +645,7 @@
.dropdown-menu-toggle {
width: 100px;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: 140px;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 7a6352e45f1..9ee02ca1d83 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -1,39 +1,56 @@
@keyframes fade-out-status {
- 0%, 50% { opacity: 1; }
- 100% { opacity: 0; }
+ 0%,
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
}
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 1);
+ 24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ 24px 0 0 0 rgba($white-light, 0.2);
}
}
@keyframes blinking-scroll-button {
- 0% { opacity: 0.2; }
- 25% { opacity: 0.5; }
- 50% { opacity: 0.7; }
- 100% { opacity: 1; }
+ 0% {
+ opacity: 0.2;
+ }
+
+ 25% {
+ opacity: 0.5;
+ }
+
+ 50% {
+ opacity: 0.7;
+ }
+
+ 100% {
+ opacity: 1;
+ }
}
.build-page {
@@ -125,12 +142,12 @@
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .3s;
+ animation-delay: 0.3s;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
- animation-delay: .2s;
+ animation-delay: 0.2s;
}
.third-triangle {
@@ -198,7 +215,7 @@
}
.header-action-buttons {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.sidebar-toggle-btn {
margin-top: 0;
margin-left: 10px;
@@ -260,10 +277,6 @@
&.coverage {
padding: 0 16px 11px;
}
-
- .btn-group-justified {
- margin-top: 5px;
- }
}
.block-last {
@@ -288,7 +301,7 @@
background-color: $white-light;
}
- .label {
+ .badge.badge-pill {
margin-left: 2px;
}
@@ -303,7 +316,7 @@
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
display: block;
.btn {
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 7b8ee026357..2b6d92016d5 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -26,3 +26,51 @@
margin-right: 0;
}
}
+
+.gcp-signup-offer {
+ background-color: $blue-50;
+ border: 1px solid $blue-300;
+ border-radius: $border-radius-default;
+
+ // TODO: To be superceded by cssLab
+ &.alert {
+ padding: 24px 16px;
+
+ &-dismissable {
+ padding-right: 32px;
+
+ .close {
+ top: -8px;
+ right: -16px;
+ color: $blue-500;
+ opacity: 1;
+ }
+ }
+ }
+
+ .gcp-logo {
+ margin-bottom: $gl-padding;
+ text-align: center;
+ }
+
+ img {
+ max-width: $gcp-signup-offer-icon-max-width;
+ }
+
+ a:not(.btn) {
+ color: $gl-link-color;
+ font-weight: normal;
+ text-decoration: none;
+ }
+
+ @include media-breakpoint-up(sm) {
+ > div {
+ display: flex;
+ align-items: center;
+ }
+
+ .gcp-logo {
+ margin: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index e9384d41e00..a4ca82de90e 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -2,7 +2,6 @@
background: none;
border: 0;
padding: 0;
- margin-top: 10px;
word-break: normal;
white-space: pre-wrap;
}
@@ -21,14 +20,10 @@
margin: 0;
color: $gl-text-color;
}
-
- .commit-description {
- margin-top: 15px;
- }
}
.commit-hash-full {
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
width: 80px;
white-space: nowrap;
overflow: hidden;
@@ -70,7 +65,7 @@
}
.branch-info .commit-icon {
- margin-right: 3px;
+ margin-right: 8px;
svg {
top: 3px;
@@ -178,7 +173,7 @@
.commit-detail {
display: flex;
justify-content: space-between;
- align-items: center;
+ align-items: start;
flex-grow: 1;
}
@@ -188,7 +183,7 @@
}
.commit-actions {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
.fa-spinner {
font-size: 12px;
}
@@ -268,20 +263,16 @@
.commit-row-description {
font-size: 14px;
- padding: 10px 15px;
- margin: 10px 0;
- background: $gray-light;
+ padding: 0 0 0 $gl-padding-8;
+ border: 0;
display: none;
white-space: pre-wrap;
word-break: normal;
-
- pre {
- border: 0;
- background: inherit;
- padding: 0;
- margin: 0;
- white-space: pre-wrap;
- }
+ color: $gl-text-color-secondary;
+ background: none;
+ font-family: inherit;
+ border-left: 2px solid $theme-gray-300;
+ border-radius: unset;
a {
color: $gl-text-color;
@@ -336,8 +327,12 @@
}
&.invalid {
- @include status-color($gray-dark, $gray, $gray-darkest);
+ @include status-color($gray-dark, color("gray"), $gray-darkest);
border-color: $gray-darkest;
+
+ &:not(span):hover {
+ color: color("gray");
+ }
}
}
diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss
index fb1899284fd..bd338326154 100644
--- a/app/assets/stylesheets/pages/convdev_index.scss
+++ b/app/assets/stylesheets/pages/convdev_index.scss
@@ -53,19 +53,19 @@ $space-between-cards: 8px;
padding: $space-between-cards / 2;
position: relative;
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
width: percentage(1 / 4);
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: percentage(1 / 5);
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: percentage(1 / 6);
}
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
width: percentage(1 / 10);
}
}
@@ -82,7 +82,7 @@ $space-between-cards: 8px;
.convdev-card-low {
border-top-color: $color-low-score;
- .card-score-big {
+ .board-card-score-big {
background-color: $red-50;
}
}
@@ -90,7 +90,7 @@ $space-between-cards: 8px;
.convdev-card-average {
border-top-color: $color-average-score;
- .card-score-big {
+ .board-card-score-big {
background-color: $orange-50;
}
}
@@ -98,7 +98,7 @@ $space-between-cards: 8px;
.convdev-card-high {
border-top-color: $color-high-score;
- .card-score-big {
+ .board-card-score-big {
background-color: $green-50;
}
}
@@ -112,14 +112,14 @@ $space-between-cards: 8px;
margin: 0 0 2px;
}
- .text-light {
+ .light-text {
font-size: 13px;
line-height: 1.25;
color: $gl-text-color-secondary;
}
}
-.card-scores {
+.board-card-scores {
display: flex;
justify-content: space-around;
align-items: center;
@@ -127,22 +127,22 @@ $space-between-cards: 8px;
line-height: 1;
}
-.card-score {
+.board-card-score {
color: $gl-text-color-secondary;
- .card-score-name {
+ .board-card-score-name {
font-size: 13px;
margin-top: 4px;
}
}
-.card-score-value {
+.board-card-score-value {
font-size: 16px;
color: $gl-text-color;
font-weight: $gl-font-weight-normal;
}
-.card-score-big {
+.board-card-score-big {
border-top: 2px solid $border-color;
border-bottom: 1px solid $border-color;
font-size: 22px;
@@ -150,7 +150,7 @@ $space-between-cards: 8px;
font-weight: $gl-font-weight-normal;
}
-.card-buttons {
+.board-card-buttons {
display: flex;
> * {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index cfef6476d4d..a22c666a525 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -15,7 +15,7 @@
max-width: 480px;
padding: 0 $gl-padding;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
margin: 0 auto;
}
}
@@ -75,13 +75,13 @@
}
}
- .panel {
+ .card {
.content-block {
padding: 24px 0;
border-bottom: 0;
position: relative;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 6px 0 24px;
}
}
@@ -89,7 +89,7 @@
.column {
text-align: center;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 15px 0;
}
@@ -106,7 +106,7 @@
}
&:last-child {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
}
}
@@ -213,7 +213,7 @@
.stage-panel {
min-width: 968px;
- .panel-heading {
+ .card-header {
padding: 0;
background-color: transparent;
}
@@ -266,7 +266,9 @@
&.issue-title,
&.commit-title,
&.merge-merquest-title {
- @include text-overflow();
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
max-width: 100%;
display: block;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 2f2c04206e2..7b36bcb3c7d 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -14,7 +14,7 @@
white-space: nowrap;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
}
}
@@ -25,7 +25,7 @@
display: flex;
flex-grow: 1;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 0;
padding-right: 0;
}
@@ -36,7 +36,7 @@
flex-shrink: 0;
flex: 0 0 auto;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
margin-top: 10px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 11052be40a8..f06c9dcdf8c 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -44,6 +44,12 @@
}
}
+ .note-text {
+ table {
+ font-family: $font-family-sans-serif;
+ }
+ }
+
table {
width: 100%;
font-family: $monospace_font;
@@ -80,13 +86,13 @@
&.left-side-selected {
td.line_content.parallel.right-side {
- @include user-select(none);
+ user-select: none;
}
}
&.right-side-selected {
td.line_content.parallel.left-side {
- @include user-select(none);
+ user-select: none;
}
}
}
@@ -103,7 +109,7 @@
.old_line,
.new_line {
- @include user-select(none);
+ user-select: none;
margin: 0;
border: 0;
padding: 0 5px;
@@ -586,14 +592,14 @@
}
.commit-stat-summary {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-left: -$gl-padding;
padding-left: $gl-padding;
background-color: $white-light;
}
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
position: -webkit-sticky;
position: sticky;
top: 24px;
@@ -604,10 +610,6 @@
top: 76px;
}
- &:not(.is-stuck) .diff-stats-additions-deletions-collapsed {
- display: none;
- }
-
&.is-stuck {
padding-top: 0;
padding-bottom: 0;
@@ -616,13 +618,21 @@
.diff-stats-additions-deletions-expanded,
.inline-parallel-buttons {
- display: none;
+ display: none !important;
+ }
+ }
+ }
+
+ @include media-breakpoint-up(lg) {
+ &.is-stuck {
+ .diff-stats-additions-deletions-collapsed {
+ display: block !important;
}
}
}
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.with-performance-bar {
.diff-files-changed.diff-files-changed-merge-request {
top: 76px + $performance-bar-height;
@@ -635,7 +645,7 @@
width: 100%;
z-index: 150;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
left: $gl-padding;
}
@@ -766,9 +776,9 @@
}
}
-.frame .badge,
-.image-diff-avatar-link .badge,
-.notes > .badge {
+.frame .badge.badge-pill,
+.image-diff-avatar-link .badge.badge-pill,
+.notes > .badge.badge-pill {
position: absolute;
background-color: $blue-400;
color: $white-light;
@@ -782,7 +792,7 @@
}
}
-.frame .badge,
+.frame .badge.badge-pill,
.frame .image-comment-badge {
// Center align badges on the frame
transform: translate(-50%, -50%);
@@ -805,14 +815,14 @@
.image-diff-avatar-link {
position: relative;
- .badge,
+ .badge.badge-pill,
.image-comment-badge {
top: 25px;
right: 8px;
}
}
-.notes > .badge {
+.notes > .badge.badge-pill {
display: none;
left: -13px;
}
@@ -834,7 +844,7 @@
display: none;
}
- .notes > .badge {
+ .notes > .badge.badge-pill {
display: block;
}
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 8ecda50602d..437621299e0 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -37,7 +37,7 @@
padding-top: 7px;
padding-bottom: 7px;
- .pull-right {
+ .float-right {
height: 20px;
}
}
@@ -63,11 +63,11 @@
max-width: 450px;
float: left;
- @media(max-width: $screen-md-max) {
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
width: 280px;
}
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
width: 180px;
}
}
@@ -111,10 +111,10 @@
}
-@media(max-width: $screen-xs-max){
+@include media-breakpoint-down(xs) {
.file-editor {
.file-title {
- .pull-right {
+ .float-right {
height: auto;
}
}
@@ -153,7 +153,7 @@
vertical-align: top;
display: inline-block;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
margin: 19px 0 12px;
}
@@ -166,7 +166,7 @@
padding: 0 0 0 14px;
border-left: 1px solid $border-color;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 5px 0;
@@ -181,7 +181,7 @@
margin-top: 6px;
line-height: 21px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
margin: 5px 0;
}
@@ -193,7 +193,7 @@
vertical-align: top;
margin: 5px 0 0 8px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 0 0 16px;
@@ -209,7 +209,7 @@
font-family: $regular_font;
margin-top: -5px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 5px 0;
@@ -223,7 +223,7 @@
width: 250px;
vertical-align: top;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 5px 0;
@@ -237,7 +237,7 @@
display: inline-block;
margin: 7px 0 0 10px;
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;
width: 100%;
margin: 20px 0;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 3a300086fa3..cd0d67613c3 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,4 +1,4 @@
-@media (max-width: $screen-md-max) {
+@include media-breakpoint-down(md) {
.deployments-container {
width: 100%;
overflow: auto;
@@ -138,7 +138,7 @@
border-left: 0;
border-right: 0;
- @media (min-width: $screen-sm-max) {
+ @media (min-width: map-get($grid-breakpoints, md)-1) {
border-top: 0;
}
}
@@ -251,7 +251,7 @@
font-size: 16px;
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
min-width: 100%;
}
}
@@ -283,28 +283,59 @@
}
&.popover {
+ padding: 0;
+ border: 1px solid $border-color;
+
&.left {
left: auto;
right: 0;
margin-right: 10px;
+
+ > .arrow {
+ right: -16px;
+ border-left-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-left-color: $theme-gray-50;
+ }
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
+
+ > .arrow {
+ left: -16px;
+ border-right-color: $border-color;
+ }
+
+ > .arrow::after {
+ border-right-color: $theme-gray-50;
+ }
}
> .arrow {
- top: 40px;
+ top: 16px;
+ margin-top: -8px;
+ border-width: 8px;
}
> .popover-title,
> .popover-content {
- padding: 5px 8px;
+ padding: 8px;
font-size: 12px;
white-space: nowrap;
}
+
+ > .popover-title {
+ background-color: $theme-gray-50;
+ }
+ }
+
+ strong {
+ font-weight: 600;
}
}
@@ -317,7 +348,7 @@
vertical-align: middle;
+ td {
- padding-left: 5px;
+ padding-left: 8px;
vertical-align: top;
}
}
@@ -401,7 +432,7 @@
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index d9267f5cdf3..f79586b68b9 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -70,7 +70,7 @@
.md {
font-size: $gl-font-size;
- .label {
+ .badge.badge-pill {
color: $gl-text-color;
}
@@ -173,7 +173,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.event-item {
padding-left: 0;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 6ee8b33bd39..c2b42e02eee 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -2,7 +2,7 @@
@include str-truncated(90%);
}
-.dashboard .side .panel .panel-heading .input-group {
+.dashboard .side .card .card-header .input-group {
.form-control {
height: 42px;
@@ -18,6 +18,10 @@
.group-row {
@include basic-list-stats;
+
+ .description p {
+ margin-bottom: 0;
+ }
}
.ldap-group-links {
@@ -36,7 +40,7 @@
flex: 1;
}
- .dropdown-menu-align-right {
+ .dropdown-menu-right {
margin-top: 0;
}
@@ -98,7 +102,7 @@
}
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
&,
.dropdown,
.dropdown .dropdown-toggle,
@@ -145,14 +149,14 @@
padding: 50px 100px;
overflow: hidden;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
padding: 50px 0;
}
svg {
float: right;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
float: none;
display: block;
width: 250px;
@@ -167,7 +171,7 @@
width: 460px;
margin-top: 120px;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
float: none;
margin-top: 60px;
width: auto;
@@ -201,7 +205,7 @@
max-width: 480px;
padding: 0 $gl-padding;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
margin: 0 auto;
}
}
@@ -237,3 +241,231 @@
overflow-y: unset;
}
}
+
+.groups-list-tree-container {
+ .has-no-search-results {
+ text-align: center;
+ padding: $gl-padding;
+ font-style: italic;
+ color: $well-light-text-color;
+ }
+
+ > .group-list-tree > .group-row.has-children:first-child {
+ border-top: 0;
+ }
+}
+
+.group-list-tree {
+ .avatar-container.content-loading {
+ position: relative;
+
+ > a,
+ > a .avatar {
+ height: 100%;
+ border-radius: 50%;
+ }
+
+ > a {
+ padding: 2px;
+
+ .avatar {
+ border: 2px solid $white-normal;
+
+ &.identicon {
+ line-height: 15px;
+ }
+ }
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background-color: transparent;
+ border: 2px outset $kdb-border;
+ border-radius: 50%;
+ animation: spin-avatar 3s infinite linear;
+ }
+ }
+
+ .folder-toggle-wrap {
+ float: left;
+ line-height: $list-text-height;
+ font-size: 0;
+
+ span {
+ font-size: $gl-font-size;
+ }
+ }
+
+ .folder-caret,
+ .item-type-icon {
+ display: inline-block;
+ }
+
+ .folder-caret {
+ width: 15px;
+
+ svg {
+ margin-bottom: 2px;
+ }
+ }
+
+ .item-type-icon {
+ margin-top: 2px;
+ width: 20px;
+ }
+
+ > .group-row:not(.has-children) {
+ .folder-caret {
+ opacity: 0;
+ }
+ }
+
+ .content-list li:last-child {
+ padding-bottom: 0;
+ }
+
+ .group-list-tree {
+ margin-bottom: 0;
+ margin-left: 30px;
+ position: relative;
+
+ &::before {
+ content: '';
+ display: block;
+ width: 0;
+ position: absolute;
+ top: 5px;
+ bottom: 0;
+ left: -16px;
+ border-left: 2px solid $border-white-normal;
+ }
+
+ .group-row {
+ position: relative;
+
+ &::before {
+ content: "";
+ display: block;
+ width: 10px;
+ height: 0;
+ border-top: 2px solid $border-white-normal;
+ position: absolute;
+ top: 30px;
+ left: -16px;
+ }
+
+ &:last-child::before {
+ background: $white-light;
+ height: auto;
+ top: 30px;
+ bottom: 0;
+ }
+
+ &.being-removed {
+ opacity: 0.5;
+ }
+ }
+ }
+
+ .group-row {
+ padding: 0;
+
+ &.has-children {
+ border-top: 0;
+ }
+
+ &:first-child {
+ border-top: 1px solid $white-normal;
+ }
+ }
+
+ .group-row-contents {
+ padding: $gl-padding-top;
+
+ &:hover {
+ border-color: $row-hover-border;
+ background-color: $row-hover;
+ cursor: pointer;
+ }
+
+ .avatar-container > a {
+ width: 100%;
+ text-decoration: none;
+ }
+
+ &.has-more-items {
+ display: block;
+ padding: 20px 10px;
+ }
+
+ .stats {
+ position: relative;
+ line-height: 46px;
+
+ > span {
+ display: inline-flex;
+ align-items: center;
+ height: 16px;
+ min-width: 30px;
+ }
+
+ > span:last-child {
+ margin-right: 0;
+ }
+
+ .stat-value {
+ margin: 2px 0 0 5px;
+ }
+ }
+
+ .controls {
+ margin-left: 5px;
+
+ > .btn {
+ margin-right: $btn-xs-side-margin;
+ }
+ }
+ }
+
+ .project-row-contents .stats {
+ line-height: inherit;
+
+ > span:first-child {
+ margin-left: 25px;
+ }
+
+ .item-visibility {
+ margin-right: 0;
+ }
+
+ .last-updated {
+ position: absolute;
+ right: 12px;
+ min-width: 250px;
+ text-align: right;
+ color: $gl-text-color-secondary;
+ }
+ }
+}
+
+ul.group-list-tree {
+ li.group-row {
+ > .group-row-contents .title {
+ line-height: $list-text-height;
+ }
+
+ &.has-description > .group-row-contents .title {
+ line-height: inherit;
+ }
+ }
+}
+
+.js-groups-list-holder {
+ .groups-list-loading {
+ font-size: 34px;
+ text-align: center;
+ }
+}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 9cc9e11bcd1..0350fe5752e 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -28,8 +28,8 @@
}
.key {
- @extend .label;
- @extend .label-inverse;
+ @extend .badge.badge-pill;
+ background-color: $label-inverse-bg;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
padding: 3px 5px;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 2c0ed976301..4aea9740735 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -187,14 +187,31 @@
padding-left: 10px;
&:hover {
- color: $gray-darkest;
+ color: $gl-text-color;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
}
}
&.assignee {
- .author_link:hover {
- .author {
- text-decoration: underline;
+ .author_link {
+ display: block;
+ padding-left: 42px;
+ position: relative;
+
+ &:hover {
+ .author {
+ text-decoration: underline;
+ }
+ }
+
+ .avatar {
+ left: 0;
+ position: absolute;
+ top: 0;
}
}
}
@@ -356,7 +373,7 @@
/* Extra small devices (phones, less than 768px) */
display: none;
/* Small devices (tablets, 768px and up) */
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: block;
}
@@ -368,6 +385,14 @@
padding: 15px 0 0;
border-bottom: 0;
overflow: hidden;
+
+ &:hover {
+ background-color: $sidebar-block-hover-color;
+ }
+
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ }
}
.participants {
@@ -380,8 +405,17 @@
.gutter-toggle {
width: 100%;
+ height: $sidebar-toggle-height;
margin-left: 0;
- padding-left: 25px;
+ padding-left: 0;
+ border-bottom: 1px solid $border-gray-dark;
+ }
+
+ a.gutter-toggle {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ text-align: center;
}
.sidebar-collapsed-icon {
@@ -428,10 +462,10 @@
.btn-clipboard {
border: 0;
+ background: transparent;
color: $issuable-sidebar-color;
&:hover {
- background: transparent;
color: $gl-text-color;
}
}
@@ -622,7 +656,7 @@
}
.issuable-form-padding-top {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-top: 7px;
}
}
@@ -636,7 +670,7 @@
padding-left: 9px;
padding-right: 9px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: inline-block;
height: auto;
align-self: center;
@@ -644,7 +678,7 @@
}
.issuable-gutter-toggle {
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
margin-left: $btn-side-margin;
}
}
@@ -662,7 +696,7 @@
width: 100%;
}
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
margin-bottom: $gl-padding;
}
}
@@ -703,7 +737,7 @@
}
}
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.issuable-meta {
.controls li {
margin-right: 0;
@@ -738,7 +772,7 @@
}
}
- @media(max-width: $screen-md-max) {
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
.task-status,
.issuable-due-date,
.project-ref-path {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index b9390450477..19fb99bfa93 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,5 +1,3 @@
-@import "./issues/issue_count_badge";
-
.issues-list {
.issue {
padding: 10px 0 10px $gl-padding;
@@ -143,7 +141,7 @@ ul.related-merge-requests > li {
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.detail-page-header {
.issuable-meta {
line-height: 18px;
@@ -205,7 +203,7 @@ ul.related-merge-requests > li {
}
}
- .btn-group:not(.hide) {
+ .btn-group:not(.hidden) {
display: flex;
}
@@ -247,7 +245,7 @@ ul.related-merge-requests > li {
display: block;
}
-@media (min-width: $screen-sm-min) {
+@include media-breakpoint-up(sm) {
.emoji-block .row {
display: flex;
@@ -258,8 +256,8 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
- .btn-group:not(.hide) {
- display: inline-block;
+ .btn-group:not(.hidden) {
+ display: inline-flex;
}
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 97985dc69c2..c25e6ce23bb 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -61,6 +61,7 @@
padding: 0 $grid-size;
line-height: 16px;
border-radius: $label-border-radius;
+ color: $white-light;
}
.dropdown-labels-error {
@@ -217,6 +218,10 @@
display: inline-flex;
vertical-align: top;
+ &:hover .color-label {
+ text-decoration: underline;
+ }
+
.label {
vertical-align: inherit;
font-size: $label-font-size;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 97303d02666..c1b1d2e028d 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -191,9 +191,9 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.login-page {
- .col-sm-5.pull-right {
+ .col-md-5.float-right {
float: none !important;
margin-bottom: 45px;
}
@@ -243,7 +243,7 @@
.navless-container {
padding: 65px 15px; // height of footer + bottom padding of email confirmation link
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding: 0 15px 65px;
}
}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 3422829de58..de2b5701e2d 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -9,9 +9,13 @@
}
}
+.member-sort-dropdown {
+ margin-left: $gl-padding-8;
+}
+
.member {
.list-item-name {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
width: 50%;
}
@@ -22,12 +26,12 @@
}
.controls {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: -webkit-flex;
display: flex;
}
- .dropdown-menu.dropdown-menu-align-right {
+ .dropdown-menu.dropdown-menu-right {
margin-top: -2px;
}
}
@@ -35,7 +39,7 @@
.form-horizontal {
margin-top: 20px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: -webkit-flex;
display: flex;
margin-top: 3px;
@@ -45,20 +49,20 @@
.btn-remove {
width: 100%;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: auto;
}
}
&.existing-title {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
}
}
}
.member-form-control {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding-bottom: 5px;
margin-left: 0;
margin-right: 0;
@@ -73,7 +77,7 @@
.member-search-form {
position: relative;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: right;
}
@@ -86,7 +90,7 @@
width: 100%;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-top: 0;
width: 155px;
}
@@ -96,19 +100,9 @@
width: 100%;
padding-right: 35px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 250px;
}
-
- &.input-short {
- @media (min-width: $screen-md-min) {
- width: 170px;
- }
-
- @media (min-width: $screen-lg-min) {
- width: 210px;
- }
- }
}
}
@@ -124,7 +118,7 @@
border: 0;
outline: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
right: 160px;
}
}
@@ -135,7 +129,7 @@
align-items: center;
justify-content: center;
- @media (max-width: $screen-sm-min) {
+ @include media-breakpoint-down(sm) {
display: block;
.flex-project-title {
@@ -151,7 +145,7 @@
text-overflow: ellipsis;
}
- .badge {
+ .badge.badge-pill {
height: 17px;
line-height: 16px;
margin-right: 5px;
@@ -166,14 +160,14 @@
}
}
-.panel {
- .panel-heading {
- .badge {
+.card {
+ .card-header {
+ .badge.badge-pill {
margin-top: 0;
}
- @media (max-width: $screen-sm-min) {
- .badge {
+ @include media-breakpoint-down(sm) {
+ .badge.badge-pill {
margin-right: 0;
margin-left: 0;
}
@@ -217,7 +211,7 @@
margin-right: 0;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
.controls > .btn {
@@ -245,7 +239,7 @@
}
}
-.panel-mobile {
+.card-mobile {
.content-list.members-list li {
display: block;
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 04bde64c752..e76525fdbf6 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -286,6 +286,14 @@ $colors: (
}
.resolve-conflicts-form {
- padding-top: $gl-padding;
+ h4 {
+ margin-top: 0;
+ }
+
+ .resolve-info {
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
+ margin-bottom: $gl-padding;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 66db4917178..9eceb3e9a33 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -131,7 +131,7 @@
color: $gl-text-color;
display: flex;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-wrap: wrap;
}
}
@@ -156,10 +156,6 @@
.dropdown-menu {
z-index: 300;
}
-
- .ci-action-icon-wrapper {
- line-height: 16px;
- }
}
.mini-pipeline-graph-dropdown-toggle {
@@ -286,7 +282,7 @@
display: inline-block;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
p {
font-size: 13px;
}
@@ -498,18 +494,18 @@
}
}
-.panel-new-merge-request {
- .panel-heading {
+.card-new-merge-request {
+ .card-header {
padding: 5px 10px;
font-weight: $gl-font-weight-bold;
line-height: 25px;
}
- .panel-body {
+ .card-body {
padding: 10px 5px;
}
- .panel-footer {
+ .card-footer {
padding: 0;
.btn {
@@ -523,7 +519,7 @@
}
.item-title {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 45%;
}
}
@@ -554,7 +550,7 @@
margin-bottom: 0;
}
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
float: left;
width: 50%;
margin-bottom: 0;
@@ -652,7 +648,7 @@
background-color: $white-light;
border-bottom: 1px solid $border-color;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
position: sticky;
position: -webkit-sticky;
}
@@ -661,7 +657,7 @@
left: 0;
transition: right .15s;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
right: 0;
}
@@ -704,7 +700,7 @@
display: flex;
justify-content: space-between;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
flex-direction: column-reverse;
}
}
@@ -740,7 +736,7 @@
display: flex;
flex-wrap: wrap;
- @media (min-width: $screen-xs) {
+ @include media-breakpoint-up(xs) {
flex-wrap: nowrap;
white-space: nowrap;
}
@@ -757,7 +753,7 @@
min-width: 100px;
max-width: 150px;
- @media (min-width: $screen-xs) {
+ @include media-breakpoint-up(xs) {
min-width: 0;
max-width: 100%;
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 3af8d80daab..dba83e56d72 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -31,7 +31,7 @@
}
}
- .panel-heading {
+ .card-header {
line-height: $line-height-base;
padding: 14px 16px;
display: -webkit-flex;
@@ -53,10 +53,6 @@
}
.milestone-sidebar {
- .gutter-toggle {
- margin-bottom: 10px;
- }
-
.milestone-progress {
.title {
padding-top: 5px;
@@ -102,7 +98,17 @@
margin-right: 0;
}
+ .right-sidebar-expanded & {
+ .gutter-toggle {
+ margin-bottom: $sidebar-milestone-toggle-bottom-margin;
+ }
+ }
+
.right-sidebar-collapsed & {
+ .milestone-progress {
+ padding-top: 0;
+ }
+
.reference {
border-top: 1px solid $border-gray-normal;
}
@@ -139,7 +145,7 @@
padding: 20px 0;
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
@@ -175,7 +181,7 @@
width: 100%;
}
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
.milestone-buttons .verbose {
display: inline;
}
@@ -223,7 +229,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.milestone-banner-text,
.milestone-banner-link {
display: inline;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 4a528bc2bb1..3b037d066dc 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -51,7 +51,7 @@
}
.note-image-attach {
- @extend .col-md-4;
+ @extend .col-lg-4;
margin-left: 45px;
float: none;
}
@@ -93,7 +93,7 @@
-webkit-flex-flow: row wrap;
width: 100%;
- .pull-right {
+ .float-right {
// Flexbox quirk to make sure right-aligned items stay right-aligned.
margin-left: auto;
}
@@ -185,12 +185,12 @@
}
.notes.notes-form > li.timeline-entry {
- @include notes-media('max', $screen-sm-max) {
+ @include notes-media('max', map-get($grid-breakpoints, md) - 1) {
padding: 0;
}
.timeline-content {
- @include notes-media('max', $screen-sm-max) {
+ @include notes-media('max', map-get($grid-breakpoints, md) - 1) {
margin: 0;
}
}
@@ -326,7 +326,7 @@
outline: 0;
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
float: left;
margin-right: $gl-padding;
@@ -350,13 +350,13 @@
line-height: 16px;
margin-top: 2px;
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
float: left;
}
}
.note-form-actions {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.btn {
float: none;
width: 100%;
@@ -375,7 +375,7 @@
left: 127px;
top: 2px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
position: relative;
top: 0;
left: 0;
@@ -410,7 +410,7 @@
width: 298px;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: flex;
width: 100%;
margin-bottom: 10px;
@@ -432,7 +432,7 @@
.uploading-container {
float: right;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
float: left;
margin-top: 5px;
}
@@ -444,7 +444,7 @@
}
.uploading-error-message {
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
&::after {
content: "\a";
white-space: pre;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 81e98f358a8..299eda53140 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -22,7 +22,7 @@ ul.notes {
.discussion-body {
padding-top: 8px;
- .panel {
+ .card {
margin-bottom: 0;
}
}
@@ -42,7 +42,7 @@ ul.notes {
position: relative;
border-bottom: 0;
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
padding-left: $note-icon-gutter-width;
}
@@ -66,7 +66,7 @@ ul.notes {
}
.timeline-icon {
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: -$note-icon-gutter-width;
}
}
@@ -74,7 +74,7 @@ ul.notes {
.timeline-content {
margin-left: $note-icon-gutter-width;
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 0;
}
}
@@ -154,7 +154,7 @@ ul.notes {
.note-header {
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, xs)) {
.inline {
display: block;
}
@@ -217,7 +217,7 @@ ul.notes {
.timeline-icon {
float: left;
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 0;
width: auto;
}
@@ -231,7 +231,7 @@ ul.notes {
}
.timeline-content {
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-left: 30px;
}
}
@@ -407,10 +407,6 @@ ul.notes {
.note-header {
display: flex;
justify-content: space-between;
-
- @include notes-media('max', $screen-xs-max) {
- flex-flow: row wrap;
- }
}
.note-header-info {
@@ -427,7 +423,7 @@ ul.notes {
}
.note-header-author-name {
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
display: none;
}
}
@@ -435,7 +431,7 @@ ul.notes {
.note-headline-light {
display: inline;
- @include notes-media('max', $screen-xs-min) {
+ @include notes-media('max', map-get($grid-breakpoints, xs)) {
display: block;
}
}
@@ -459,6 +455,10 @@ ul.notes {
white-space: normal;
}
+ .system-note-separator {
+ color: $gl-text-color-disabled;
+ }
+
a:hover {
text-decoration: underline;
}
@@ -473,11 +473,6 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
- @include notes-media('max', $screen-md-max) {
- float: none;
- margin-left: 0;
- }
-
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
@@ -491,7 +486,7 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none;
margin-left: 0;
}
@@ -639,6 +634,10 @@ ul.notes {
margin-left: -55px;
position: absolute;
z-index: 10;
+
+ .new & {
+ margin-top: -10px;
+ }
}
.discussion-body,
@@ -673,7 +672,7 @@ ul.notes {
}
.line-resolve-all-container {
- @include notes-media('min', $screen-sm-min) {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-right: 0;
padding-left: $gl-padding;
}
@@ -772,7 +771,3 @@ ul.notes {
height: auto;
}
}
-
-.line-resolve-text {
- vertical-align: middle;
-}
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index bdf07a99daf..e98cb444f0a 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -2,7 +2,7 @@
line-height: 34px;
.dropdown-menu {
- @extend .dropdown-menu-align-right;
+ @extend .dropdown-menu-right;
}
}
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index bc7fa8a26d9..86e70955389 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -69,7 +69,7 @@
.cron-preset-radio-input {
display: inline-block;
- @media (max-width: $screen-md-max) {
+ @include media-breakpoint-down(md) {
display: block;
margin: 0 0 5px 5px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 855ebf7d86d..f85f66b9c0b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1,3 +1,30 @@
+@mixin flat-connector-before($length: 44px) {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -$length;
+ border-top: 2px solid $border-color;
+ width: $length;
+ height: 1px;
+ }
+}
+
+@mixin build-content($border-radius: 30px) {
+ display: inline-block;
+ padding: 8px 10px 9px;
+ width: 100%;
+ border: 1px solid $border-color;
+ border-radius: $border-radius;
+ background-color: $white-light;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+ color: $gl-text-color;
+ }
+}
+
.pipelines {
.stage {
max-width: 90px;
@@ -16,14 +43,13 @@
margin: 0;
white-space: normal;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
justify-content: flex-end;
}
}
.ci-table {
-
- .label {
+ .badge {
margin-bottom: 3px;
}
@@ -67,13 +93,9 @@
}
}
- .btn-group {
- &.open {
- .btn-default {
- background-color: $white-normal;
- border-color: $border-white-normal;
- }
- }
+ .btn-group.open .btn-default {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
}
.btn .text-center {
@@ -87,7 +109,7 @@
}
}
-@media (max-width: $screen-md-max) {
+@include media-breakpoint-down(md) {
.content-list {
&.builds-content-list {
width: 100%;
@@ -123,7 +145,6 @@
}
.branch-commit {
-
.ref-name {
font-weight: $gl-font-weight-bold;
max-width: 100px;
@@ -156,14 +177,14 @@
color: $gl-link-color;
}
- .label {
+ .badge {
margin-right: 4px;
}
.label-container {
font-size: 0;
- .label {
+ .badge {
margin-top: 5px;
}
}
@@ -232,7 +253,7 @@
.stage-cell {
&.table-section {
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
min-width: 160px; /* Hack alert: Without this the mini graph pipeline won't work properly*/
margin-right: -4px;
}
@@ -364,15 +385,7 @@
margin-left: 44px;
.left-connector {
- &::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -44px;
- border-top: 2px solid $border-color;
- width: 44px;
- height: 1px;
- }
+ @include flat-connector-before;
}
}
}
@@ -388,22 +401,16 @@
&:last-child {
.build {
// Remove right connecting horizontal line from first build in last stage
- &:first-child {
- &::after {
- border: 0;
- }
+ &:first-child::after {
+ border: 0;
}
// Remove right curved connectors from all builds in last stage
- &:not(:first-child) {
- &::after {
- border: 0;
- }
+ &:not(:first-child)::after {
+ border: 0;
}
// Remove opposite curve
- .curve {
- &::before {
- display: none;
- }
+ .curve::before {
+ display: none;
}
}
}
@@ -411,16 +418,12 @@
&:first-child {
.build {
// Remove left curved connectors from all builds in first stage
- &:not(:first-child) {
- &::before {
- border: 0;
- }
+ &:not(:first-child)::before {
+ border: 0;
}
// Remove opposite curve
- .curve {
- &::after {
- display: none;
- }
+ .curve::after {
+ display: none;
}
}
}
@@ -468,48 +471,19 @@
margin-bottom: 10px;
white-space: normal;
+ .ci-job-dropdown-container {
+ // override dropdown.scss
+ .dropdown-menu li button {
+ padding: 0;
+ text-align: center;
+ }
+ }
+
// ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover;
}
- .ci-action-icon-container {
- position: absolute;
- right: 5px;
- top: 5px;
-
- // Action Icons in big pipeline-graph nodes
- &.ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
- background: $white-light;
- border: 1px solid $border-color;
- border-radius: 100%;
- display: block;
-
- &:hover {
- background-color: $stage-hover-bg;
- border: 1px solid $dropdown-toggle-active-border-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
-
- svg {
- fill: $gl-text-color-secondary;
- position: relative;
- top: -1px;
- }
-
- &.play {
- svg {
- left: 2px;
- }
- }
- }
- }
-
.ci-status-icon svg {
height: 20px;
width: 20px;
@@ -526,12 +500,7 @@
}
.build-content {
- display: inline-block;
- padding: 8px 10px 9px;
- width: 100%;
- border: 1px solid $border-color;
- border-radius: 30px;
- background-color: $white-light;
+ @include build-content();
}
a.build-content:hover,
@@ -540,7 +509,6 @@
border: 1px solid $dropdown-toggle-active-border-color;
}
-
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
@@ -594,6 +562,43 @@
}
}
}
+
+ .ci-action-icon-container {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+
+ // Action Icons in big pipeline-graph nodes
+ &.ci-action-icon-wrapper {
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ svg {
+ fill: $gl-text-color-secondary;
+ position: relative;
+ top: -1px;
+ }
+
+ &.play {
+ svg {
+ left: 2px;
+ }
+ }
+ }
+ }
}
// Triggers the dropdown in the big pipeline graph
@@ -633,8 +638,7 @@
}
}
-// Dropdown button in mini pipeline graph
-button.mini-pipeline-graph-dropdown-toggle {
+@mixin mini-pipeline-item() {
border-radius: 100px;
background-color: $white-light;
border-width: 1px;
@@ -647,30 +651,6 @@ button.mini-pipeline-graph-dropdown-toggle {
position: relative;
vertical-align: middle;
- > .fa.fa-caret-down {
- position: absolute;
- left: 20px;
- top: 5px;
- display: inline-block;
- visibility: hidden;
- opacity: 0;
- color: inherit;
- font-size: 12px;
- transition: visibility 0.1s, opacity 0.1s linear;
- }
-
- &:active,
- &:focus,
- &:hover {
- outline: none;
- width: 35px;
-
- .fa.fa-caret-down {
- visibility: visible;
- opacity: 1;
- }
- }
-
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
@include mini-pipeline-graph-color($green-100, $green-500, $green-600);
@@ -702,93 +682,106 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
-// dropdown content for big and mini pipeline
+// Dropdown button in mini pipeline graph
+button.mini-pipeline-graph-dropdown-toggle {
+ @include mini-pipeline-item();
+
+ > .fa.fa-caret-down {
+ position: absolute;
+ left: 20px;
+ top: 5px;
+ display: inline-block;
+ visibility: hidden;
+ opacity: 0;
+ color: inherit;
+ font-size: 12px;
+ transition: visibility 0.1s, opacity 0.1s linear;
+ }
+
+ &:active,
+ &:focus,
+ &:hover {
+ outline: none;
+ width: 35px;
+
+ .fa.fa-caret-down {
+ visibility: visible;
+ opacity: 1;
+ }
+ }
+}
+
+/**
+ Action icons inside dropdowns:
+ - mini graph in pipelines table
+ - dropdown in big graph
+ - mini graph in MR widget pipeline
+ - mini graph in Commit widget pipeline
+*/
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
width: 240px;
max-width: 240px;
- .scrollable-menu {
+ // override dropdown.scss
+ &.dropdown-menu li button,
+ &.dropdown-menu li a.ci-action-icon-container {
padding: 0;
- max-height: 245px;
- overflow: auto;
+ text-align: center;
}
- li {
- position: relative;
+ .ci-action-icon-container {
+ position: absolute;
+ right: 8px;
+ top: 8px;
- // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
- &:hover > .mini-pipeline-graph-dropdown-item,
- &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
- @extend .mini-pipeline-graph-dropdown-item:hover;
- }
+ &.ci-action-icon-wrapper {
+ height: $ci-action-dropdown-button-size;
+ width: $ci-action-dropdown-button-size;
- // Action icon on the right
- a.ci-action-icon-wrapper {
- border-radius: 50%;
+ background: $white-light;
border: 1px solid $border-color;
- width: $ci-action-icon-size;
- height: $ci-action-icon-size;
- padding: 2px 0 0 5px;
- font-size: 12px;
- background-color: $white-light;
- position: absolute;
- top: 50%;
- right: $gl-padding;
- margin-top: -#{$ci-action-icon-size / 2};
+ border-radius: 50%;
+ display: block;
- &:hover,
- &:focus {
+ &:hover {
background-color: $stage-hover-bg;
border: 1px solid $dropdown-toggle-active-border-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
}
svg {
+ width: $ci-action-dropdown-svg-size;
+ height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
- width: #{$ci-action-icon-size - 6};
- height: #{$ci-action-icon-size - 6};
- left: -3px;
position: relative;
- top: -1px;
-
- &.icon-action-stop,
- &.icon-action-cancel {
- width: 12px;
- height: 12px;
- top: 1px;
- left: -1px;
- }
-
- &.icon-action-play {
- width: 11px;
- height: 11px;
- top: 1px;
- left: 1px;
- }
-
- &.icon-action-retry {
- width: 16px;
- height: 16px;
- top: 0;
- left: -3px;
- }
+ top: 0;
+ vertical-align: initial;
}
+ }
+ }
- &:hover svg,
- &:focus svg {
- fill: $gl-text-color;
- }
+ // SVGs in the commit widget and mr widget
+ a.ci-action-icon-container.ci-action-icon-wrapper svg {
+ top: 2px;
+ }
- &.icon-action-retry,
- &.icon-action-play {
- svg {
- width: #{$ci-action-icon-size - 6};
- height: #{$ci-action-icon-size - 6};
- left: 8px;
- }
- }
+ .scrollable-menu {
+ padding: 0;
+ max-height: 245px;
+ overflow: auto;
+ }
+ li {
+ position: relative;
+ // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
+ &:hover > .mini-pipeline-graph-dropdown-item,
+ &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
+ @extend .mini-pipeline-graph-dropdown-item:hover;
}
// link to the build
@@ -800,6 +793,11 @@ button.mini-pipeline-graph-dropdown-toggle {
line-height: $line-height-base;
white-space: nowrap;
+ // Match dropdown.scss for all `a` tags
+ &.non-details-job-component {
+ padding: 8px 16px;
+ }
+
.ci-job-name-component {
align-items: center;
display: flex;
@@ -822,7 +820,7 @@ button.mini-pipeline-graph-dropdown-toggle {
display: block;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 60%;
}
}
@@ -909,7 +907,7 @@ button.mini-pipeline-graph-dropdown-toggle {
transform: translate(-50%, 0);
border-width: 0 5px 6px;
- @media (max-width: $screen-sm-max) {
+ @include media-breakpoint-down(sm) {
left: 100%;
margin-left: -12px;
}
@@ -931,7 +929,7 @@ button.mini-pipeline-graph-dropdown-toggle {
&.dropdown-menu {
transform: translate(-80%, 0);
- @media(min-width: $screen-md-min) {
+ @media(min-width: map-get($grid-breakpoints, md)) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index b199f9876d3..06078f1d12e 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -5,7 +5,7 @@
}
.avatar-image {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
margin-bottom: 0;
}
@@ -119,7 +119,7 @@
.key-list-item {
.key-list-item-info {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
float: left;
}
}
@@ -188,7 +188,7 @@
.modal-dialog {
width: 380px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: auto;
}
@@ -242,7 +242,7 @@
left: 0;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.cover-block {
padding-top: 20px;
}
@@ -352,7 +352,7 @@ table.u2f-registrations {
}
}
- @media(max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
text-align: center;
.bordered-box {
@@ -414,7 +414,7 @@ table.u2f-registrations {
}
&.unverified {
- @include status-color($gray-dark, $gray, $common-gray-dark);
+ @include status-color($gray-dark, color("gray"), $common-gray-dark);
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index d7d343b088a..22964163e95 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -9,7 +9,7 @@
.new_project,
.edit-project,
.import-project {
- .help-block {
+ .form-text.text-muted {
margin-bottom: 10px;
}
@@ -34,7 +34,7 @@
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.input-group > div {
&:last-child {
margin-bottom: 0;
@@ -46,7 +46,8 @@
}
}
- .input-group-addon {
+ .input-group-prepend,
+ .input-group-append {
overflow: hidden;
text-overflow: ellipsis;
line-height: unset;
@@ -82,7 +83,7 @@
border: 1px solid $border-color;
padding: 10px 32px;
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
padding: 10px 20px;
}
}
@@ -134,7 +135,7 @@
max-width: 400px;
}
- @media (max-width: $screen-xs-min) {
+ @include media-breakpoint-down(xs) {
padding-left: 20px;
}
}
@@ -144,7 +145,7 @@
padding-top: 24px;
padding-bottom: 24px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
border-bottom: 1px solid $border-color;
}
@@ -205,7 +206,6 @@
.project-repo-buttons,
.group-buttons {
.btn {
- @include btn-gray;
padding: 3px 10px;
&:last-child {
@@ -231,11 +231,11 @@
}
.notification-dropdown .dropdown-menu {
- @extend .dropdown-menu-align-right;
+ @extend .dropdown-menu-right;
}
.download-button {
- @media (max-width: $screen-md-max) {
+ @include media-breakpoint-down(md) {
margin-left: 0;
}
}
@@ -294,7 +294,7 @@
}
.count {
- @include btn-gray;
+ @include btn-white;
display: inline-block;
background: $white-light;
border-radius: 2px;
@@ -354,30 +354,48 @@
min-width: 200px;
}
-.deploy-key-content {
- @media (min-width: $screen-sm-min) {
- float: left;
+.deploy-keys {
+ .scrolling-tabs-container {
+ position: relative;
+ }
+}
- &:last-child {
- float: right;
+.deploy-key {
+ // Ensure that the fingerprint does not overflow on small screens
+ .fingerprint {
+ word-break: break-all;
+ white-space: normal;
+ }
+
+ .deploy-project-label,
+ .key-created-at {
+ svg {
+ vertical-align: text-top;
}
}
-}
-.deploy-key-projects {
- @media (min-width: $screen-sm-min) {
- line-height: 42px;
+ .btn svg {
+ vertical-align: top;
+ }
+
+ .key-created-at {
+ line-height: unset;
}
}
-a.deploy-project-label {
- padding: 5px;
- margin-right: 5px;
- color: $gl-text-color;
- background-color: $row-hover;
+.deploy-project-list {
+ margin-bottom: -$gl-padding-4;
- &:hover {
- color: $gl-link-color;
+ a.deploy-project-label {
+ margin-right: $gl-padding-4;
+ margin-bottom: $gl-padding-4;
+ color: $gl-text-color-secondary;
+ background-color: $theme-gray-100;
+ line-height: $gl-btn-line-height;
+
+ &:hover {
+ color: $gl-link-color;
+ }
}
}
@@ -427,11 +445,11 @@ a.deploy-project-label {
height: 200px;
width: calc((100% / 2) - #{$gl-padding * 2});
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
width: calc((100% / 4) - #{$gl-padding * 2});
}
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
width: calc((100% / 5) - #{$gl-padding * 2});
}
@@ -520,11 +538,12 @@ a.deploy-project-label {
.template-input-group {
position: relative;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: flex;
}
- .input-group-addon {
+ .input-group-prepend,
+ .input-group-append {
flex: 1;
text-align: left;
padding-left: ($gl-padding * 3);
@@ -584,12 +603,12 @@ a.deploy-project-label {
margin: 0 auto 4px;
font-size: 24px;
- @media (min-width: $screen-xs-max) {
+ @media (min-width: map-get($grid-breakpoints, sm)-1) {
top: 0;
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.btn-template-icon {
display: inline-block;
height: 14px;
@@ -608,31 +627,31 @@ a.deploy-project-label {
.create-project-options {
display: flex;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
display: block;
}
.first-column {
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
max-width: 50%;
padding-right: 30px;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 100%;
width: 100%;
}
}
.second-column {
- @media (min-width: $screen-xs-min) {
+ @include media-breakpoint-up(xs) {
width: 50%;
flex: 1;
padding-left: 30px;
position: relative;
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
max-width: 100%;
width: 100%;
padding-left: 0;
@@ -640,7 +659,7 @@ a.deploy-project-label {
}
// Mobile
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
padding-top: 30px;
}
@@ -660,7 +679,7 @@ a.deploy-project-label {
line-height: 20px;
// Mobile
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
left: 50%;
top: 0;
transform: translateX(-50%);
@@ -680,7 +699,7 @@ a.deploy-project-label {
top: 0;
// Mobile
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
top: 10px;
left: 10px;
right: 10px;
@@ -718,7 +737,7 @@ a.deploy-project-label {
vertical-align: top;
margin-top: 0;
- @media (min-width: $screen-lg-min) {
+ @include media-breakpoint-up(lg) {
float: right;
}
}
@@ -849,19 +868,14 @@ pre.light-well {
}
}
-.panel .projects-list li {
+.card .projects-list li {
padding: 10px 15px;
margin: 0;
}
-.commits-search-form {
- .input-short {
- min-width: 200px;
- }
-}
-
.git-clone-holder {
width: 380px;
+ height: 28px;
.btn-clipboard {
border: 1px solid $border-color;
@@ -877,11 +891,11 @@ pre.light-well {
.form-control {
@extend .monospace;
- background: $white-light;
+ background-color: $white-light;
+ border-color: $border-color;
font-size: 14px;
margin-left: -1px;
cursor: auto;
- width: 101%;
}
}
@@ -910,7 +924,8 @@ pre.light-well {
}
.project-tip-command {
- > .input-group-btn:first-child {
+ > .input-group-prepend:first-child,
+ > .input-group-append:first-child {
width: auto;
}
}
@@ -961,7 +976,7 @@ pre.light-well {
.dropdown-menu-projects {
width: 300px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 500px;
}
@@ -975,7 +990,7 @@ pre.light-well {
.inline-input-group {
width: 100%;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 300px;
}
}
@@ -986,8 +1001,8 @@ pre.light-well {
text-align: center;
margin-top: -20px;
- @media (min-width: $screen-sm-min) {
- margin-top: 0;
+ @include media-breakpoint-up(sm) {
+ margin: 0 $gl-padding-8;
width: auto;
}
}
@@ -1025,7 +1040,7 @@ pre.light-well {
}
&.form-group {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-bottom: 0;
}
}
@@ -1053,12 +1068,12 @@ pre.light-well {
.project-feature {
padding-top: 10px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 45px;
}
&.nested {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-left: 90px;
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index f58f2579050..17d7087bd85 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -17,11 +17,12 @@
}
.ide-view {
+ position: relative;
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 0;
border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
+ padding-bottom: $ide-statusbar-height;
&.is-collapsed {
.ide-file-list {
@@ -37,12 +38,15 @@
.ide-file-list {
flex: 1;
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+ padding-bottom: $grid-size;
.file {
cursor: pointer;
&.file-open {
- background: $link-active-background;
+ background: $white-normal;
}
&.file-active {
@@ -54,6 +58,7 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
+ line-height: 22px;
svg {
vertical-align: middle;
@@ -66,29 +71,34 @@
}
}
+ .ide-file-icon-holder {
+ display: flex;
+ align-items: center;
+ }
+
.ide-file-changed-icon {
margin-left: auto;
+
+ > svg {
+ display: block;
+ }
}
.ide-new-btn {
display: none;
- margin-bottom: -4px;
- margin-right: -8px;
}
&:hover,
&:focus {
- background: $link-active-background;
+ background: $white-normal;
.ide-new-btn {
display: block;
}
}
- &.folder {
- svg {
- fill: $gl-text-color-secondary;
- }
+ .folder-icon {
+ fill: $gl-text-color-secondary;
}
}
@@ -102,24 +112,16 @@
}
}
-.file-name,
-.file-col-commit-message {
+.file-name {
display: flex;
overflow: visible;
- padding: 6px 12px;
+ align-items: center;
+ width: 100%;
}
.multi-file-loading-container {
margin-top: 10px;
padding: 10px;
-
- .animation-container {
- background: $gray-light;
-
- div {
- background: $gray-light;
- }
- }
}
.multi-file-table-col-commit-message {
@@ -146,69 +148,56 @@
}
li {
- position: relative;
- }
-
- .dropdown {
display: flex;
- margin-left: auto;
- margin-bottom: 1px;
- padding: 0 $grid-size;
- border-left: 1px solid $white-dark;
- background-color: $white-light;
-
- &.shadow {
- box-shadow: 0 0 10px $dropdown-shadow-color;
- }
+ align-items: center;
+ padding: $grid-size $gl-padding;
+ background-color: $gray-normal;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
- .btn {
- margin-top: auto;
- margin-bottom: auto;
+ &.active {
+ background-color: $white-light;
+ border-bottom-color: $white-light;
}
}
}
.multi-file-tab {
- @include str-truncated(150px);
- padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
- background-color: $gray-normal;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
+ @include str-truncated(141px);
cursor: pointer;
svg {
vertical-align: middle;
}
-
- &.active {
- background-color: $white-light;
- border-bottom-color: $white-light;
- }
}
.multi-file-tab-close {
- position: absolute;
- right: 8px;
- top: 50%;
width: 16px;
height: 16px;
padding: 0;
+ margin-left: $grid-size;
background: none;
border: 0;
border-radius: $border-radius-default;
color: $theme-gray-900;
- transform: translateY(-50%);
svg {
position: relative;
top: -1px;
}
- &:hover {
+ .ide-file-changed-icon {
+ display: block;
+ position: relative;
+ top: 1px;
+ right: -2px;
+ }
+
+ &:not([disabled]):hover {
background-color: $theme-gray-200;
}
- &:focus {
+ &:not([disabled]):focus {
background-color: $blue-500;
color: $white-light;
outline: 0;
@@ -239,6 +228,17 @@
display: none;
}
+ .is-readonly,
+ .editor.original {
+ .view-lines {
+ cursor: default;
+ }
+
+ .cursors-layer {
+ display: none;
+ }
+ }
+
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
@@ -297,20 +297,27 @@
.margin-view-overlays .delete-sign {
opacity: 0.4;
}
-
- .cursors-layer {
- display: none;
- }
}
}
.multi-file-editor-holder {
height: 100%;
+ min-height: 0;
}
.preview-container {
- height: 100%;
- overflow: auto;
+ flex-grow: 1;
+ position: relative;
+
+ .md-previewer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ padding: $gl-padding;
+ }
.file-container {
background-color: $gray-darker;
@@ -350,10 +357,6 @@
color: $diff-image-info-color;
}
}
-
- .md-previewer {
- padding: $gl-padding;
- }
}
.ide-mode-tabs {
@@ -371,20 +374,46 @@
.ide-btn-group {
padding: $gl-padding-4 $gl-vert-padding;
+ line-height: 24px;
}
.ide-status-bar {
border-top: 1px solid $white-dark;
- padding: $gl-bar-padding $gl-padding;
+ padding: 2px $gl-padding-8 0;
background: $white-light;
display: flex;
justify-content: space-between;
+ height: $ide-statusbar-height;
+
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+
+ font-size: 12px;
+ line-height: 22px;
+
+ * {
+ font-size: inherit;
+ }
+
+ > div + div {
+ padding-left: $gl-padding;
+ }
svg {
- vertical-align: middle;
+ vertical-align: sub;
}
}
+.ide-status-file {
+ text-align: right;
+
+ .ide-status-branch + &,
+ &:first-child {
+ margin-left: auto;
+ }
+}
// Not great, but this is to deal with our current output
.multi-file-preview-holder {
height: 100%;
@@ -420,27 +449,35 @@
.multi-file-commit-panel {
display: flex;
position: relative;
- flex-direction: column;
width: 340px;
padding: 0;
background-color: $gray-light;
- padding-right: 3px;
+ padding-right: 1px;
+
+ .context-header {
+ width: auto;
+ margin-right: 0;
+
+ a:hover,
+ a:focus {
+ text-decoration: none;
+ }
+ }
.projects-sidebar {
+ min-height: 0;
display: flex;
flex-direction: column;
- height: 100%;
-
- .context-header {
- width: auto;
- margin-right: 0;
- }
+ flex: 1;
}
.multi-file-commit-panel-inner {
+ position: relative;
display: flex;
flex-direction: column;
height: 100%;
+ min-width: 0;
+ width: 100%;
}
.multi-file-commit-panel-inner-scroll {
@@ -448,68 +485,10 @@
flex: 1;
flex-direction: column;
overflow: auto;
- }
-
- &.is-collapsed {
- width: 60px;
-
- .multi-file-commit-list {
- padding-top: $gl-padding;
- overflow: hidden;
- }
-
- .multi-file-context-bar-icon {
- align-items: center;
-
- svg {
- float: none;
- margin: 0;
- }
- }
- }
-
- .branch-container {
- border-left: 4px solid;
- margin-bottom: $gl-bar-padding;
- }
-
- .branch-header {
- background: $white-dark;
- display: flex;
- }
-
- .branch-header-title {
- flex: 1;
- padding: $grid-size $gl-padding;
- font-weight: $gl-font-weight-bold;
-
- svg {
- vertical-align: middle;
- }
- }
-
- .branch-header-btns {
- padding: $gl-vert-padding $gl-padding;
- }
-
- .left-collapse-btn {
- display: none;
- background: $gray-light;
- text-align: left;
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
border-top: 1px solid $white-dark;
-
- svg {
- vertical-align: middle;
- }
- }
-}
-
-.multi-file-context-bar-icon {
- padding: 10px;
-
- svg {
- margin-right: 10px;
- float: left;
+ border-top-left-radius: $border-radius-small;
}
}
@@ -521,9 +500,13 @@
overflow: auto;
}
-.multi-file-commit-empty-state-container {
- align-items: center;
- justify-content: center;
+.ide-commit-empty-state {
+ padding: 0 $gl-padding;
+}
+
+.ide-commit-empty-state-container {
+ margin-top: auto;
+ margin-bottom: auto;
}
.multi-file-commit-panel-header {
@@ -531,86 +514,79 @@
align-items: center;
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding 0;
-
- &.is-collapsed {
- border-bottom: 1px solid $white-dark;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
+ padding: 12px 0;
}
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
- padding: 0 $gl-btn-padding;
+ align-items: center;
svg {
margin-right: $gl-btn-padding;
+ color: $theme-gray-700;
}
}
.multi-file-commit-panel-collapse-btn {
border-left: 1px solid $white-dark;
+ margin-left: auto;
}
.multi-file-commit-list {
flex: 1;
overflow: auto;
- padding: $gl-padding 0;
+ padding: $grid-size 0;
+ margin-left: -$grid-size;
+ margin-right: -$grid-size;
min-height: 60px;
+
+ .multi-file-commit-list-item {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ &.help-block {
+ margin-left: 0;
+ right: 0;
+ }
}
.multi-file-commit-list-item {
- display: flex;
- padding: 0;
- align-items: center;
-
.multi-file-discard-btn {
display: none;
+ margin-top: -2px;
margin-left: auto;
color: $gl-link-color;
- padding: 0 2px;
-
- &:focus,
- &:hover {
- text-decoration: underline;
- }
}
&:hover {
- background: $white-normal;
-
.multi-file-discard-btn {
- display: block;
+ display: flex;
}
}
}
-.multi-file-addition {
- fill: $green-500;
+.multi-file-addition,
+.multi-file-addition-solid {
+ color: $green-500;
}
-.multi-file-modified {
- fill: $orange-500;
+.multi-file-modified,
+.multi-file-modified-solid {
+ color: $orange-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
+ padding: $gl-padding 0;
- > svg {
+ svg {
+ display: block;
margin-left: auto;
margin-right: auto;
+ color: $theme-gray-700;
}
.file-status-icon {
@@ -620,25 +596,39 @@
}
}
+.multi-file-commit-list-item,
+.ide-file-list .file {
+ display: flex;
+ align-items: center;
+ margin-left: -$grid-size;
+ margin-right: -$grid-size;
+ padding: $grid-size / 2 $grid-size;
+ border-radius: $border-radius-default;
+ text-align: left;
+
+ &:hover,
+ &:focus {
+ background: $white-normal;
+ }
+}
+
.multi-file-commit-list-path {
- padding: $grid-size / 2;
- padding-left: $gl-padding;
+ padding: 0;
background: none;
border: 0;
text-align: left;
width: 100%;
- min-width: 0;
+
+ &:hover,
+ &:focus {
+ outline: 0;
+ }
svg {
min-width: 16px;
vertical-align: middle;
display: inline-block;
}
-
- &:hover,
- &:focus {
- outline: 0;
- }
}
.multi-file-commit-list-file-path {
@@ -654,12 +644,30 @@
}
.multi-file-commit-form {
- padding: $gl-padding;
- border-top: 1px solid $white-dark;
+ position: relative;
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
+ transition: all 0.3s ease;
+
+ > form,
+ > .commit-form-compact {
+ padding: $gl-padding 0;
+ margin-left: $gl-padding;
+ margin-right: $gl-padding;
+ border-top: 1px solid $white-dark;
+ }
.btn {
font-size: $gl-font-size;
}
+
+ .multi-file-commit-panel-success-message {
+ top: 0;
+ }
+}
+
+.multi-file-commit-panel-bottom {
+ position: relative;
}
.dirty-diff {
@@ -795,7 +803,7 @@
position: absolute;
top: 0;
bottom: 0;
- width: 3px;
+ width: 1px;
background-color: $white-dark;
&.dragright {
@@ -807,12 +815,46 @@
}
}
+.ide-commit-list-container {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-height: 140px;
+ margin-left: $gl-padding;
+ margin-right: $gl-padding;
+
+ &.is-first {
+ border-bottom: 1px solid $white-dark;
+ }
+}
+
+.ide-staged-action-btn {
+ margin-left: auto;
+ line-height: 22px;
+}
+
+.ide-commit-file-count {
+ min-width: 22px;
+ margin-left: auto;
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ border: 1px solid $white-dark;
+ line-height: 20px;
+ text-align: center;
+}
+
.ide-commit-radios {
label {
font-weight: normal;
+
+ &.is-disabled {
+ .ide-radio-label {
+ text-decoration: line-through;
+ }
+ }
}
- .help-block {
+ .form-text.text-muted {
margin-top: 0;
line-height: 0;
}
@@ -822,17 +864,78 @@
margin-left: 25px;
}
-.ide-external-links {
- p {
- margin: 0;
- }
-}
-
.ide-sidebar-link {
- padding: $gl-padding-8 $gl-padding;
display: flex;
align-items: center;
- font-weight: $gl-font-weight-bold;
+ position: relative;
+ height: 60px;
+ width: 100%;
+ padding: 0 $gl-padding;
+ color: $gl-text-color-secondary;
+ background-color: transparent;
+ border: 0;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ outline: 0;
+
+ svg {
+ margin: 0 auto;
+ }
+
+ &:hover {
+ color: $gl-text-color;
+ background-color: $theme-gray-100;
+ }
+
+ &:focus {
+ color: $gl-text-color;
+ background-color: $theme-gray-200;
+ }
+
+ &.active {
+ // extend width over border of sidebar section
+ width: calc(100% + 1px);
+ padding-right: $gl-padding + 1px;
+ background-color: $white-light;
+ border-top-color: $white-dark;
+ border-bottom-color: $white-dark;
+
+ &::after {
+ content: '';
+ position: absolute;
+ right: -1px;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: $white-light;
+ }
+ }
+}
+
+.ide-activity-bar {
+ position: relative;
+ flex: 0 0 60px;
+ z-index: 1;
+}
+
+.ide-file-finder-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 100;
+}
+
+.ide-file-finder {
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+
+ .highlighted {
+ color: $blue-500;
+ font-weight: $gl-font-weight-bold;
+ }
}
.ide-commit-message-field {
@@ -905,3 +1008,120 @@
background: transparent;
resize: none;
}
+
+.ide-tree-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ padding: 12px 0;
+ border-bottom: 1px solid $white-dark;
+
+ .ide-new-btn {
+ margin-left: auto;
+ }
+}
+
+.ide-sidebar-branch-title {
+ font-weight: $gl-font-weight-normal;
+
+ svg {
+ position: relative;
+ top: 3px;
+ margin-top: -1px;
+ }
+}
+
+.commit-form-compact {
+ .btn {
+ margin-bottom: 8px;
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+}
+
+.commit-form-slide-up-enter-active,
+.commit-form-slide-up-leave-active {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ transition: all 0.3s ease;
+}
+
+.is-full .commit-form-slide-up-enter,
+.is-compact .commit-form-slide-up-leave-to {
+ transform: translateY(100%);
+}
+
+.is-full .commit-form-slide-up-enter-to,
+.is-compact .commit-form-slide-up-leave {
+ transform: translateY(0);
+}
+
+.commit-form-slide-up-enter,
+.commit-form-slide-up-leave-to {
+ opacity: 0;
+}
+
+.ide-review-header {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .dropdown {
+ margin-left: auto;
+ }
+
+ a {
+ color: $gl-link-color;
+ }
+}
+
+.ide-review-sub-header {
+ color: $gl-text-color-secondary;
+}
+
+.ide-tree-changes {
+ display: flex;
+ align-items: center;
+ font-size: 12px;
+}
+
+.ide-new-modal-label {
+ line-height: 34px;
+}
+
+.multi-file-commit-panel-success-message {
+ position: absolute;
+ top: 61px;
+ left: 1px;
+ bottom: 0;
+ right: 0;
+ z-index: 10;
+ background: $white-light;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.ide-review-button-holder {
+ display: flex;
+ width: 100%;
+ align-items: center;
+}
+
+.ide-context-header {
+ .avatar {
+ flex: 0 0 40px;
+ }
+}
+
+.ide-sidebar-project-title {
+ min-width: 0;
+
+ .sidebar-context-title {
+ white-space: nowrap;
+ }
+}
diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig
deleted file mode 100644
index 57b995adb64..00000000000
--- a/app/assets/stylesheets/pages/repo.scss.orig
+++ /dev/null
@@ -1,786 +0,0 @@
-.project-refs-form,
-.project-refs-target-form {
- display: inline-block;
-}
-
-.fade-enter,
-.fade-leave-to {
- opacity: 0;
-}
-
-.commit-message {
- @include str-truncated(250px);
-}
-
-.editable-mode {
- display: inline-block;
-}
-
-.ide-view {
- display: flex;
- height: calc(100vh - #{$header-height});
- margin-top: 40px;
- color: $almost-black;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
-
- &.is-collapsed {
- .ide-file-list {
- max-width: 250px;
- }
- }
-
- .file-status-icon {
- width: 10px;
- height: 10px;
- }
-}
-
-.ide-file-list {
- flex: 1;
-
- .file {
- cursor: pointer;
-
- &.file-open {
- background: $white-normal;
- }
-
- .ide-file-name {
- flex: 1;
- white-space: nowrap;
- text-overflow: ellipsis;
-
- svg {
- vertical-align: middle;
- margin-right: 2px;
- }
-
- .loading-container {
- margin-right: 4px;
- display: inline-block;
- }
- }
-
- .ide-file-changed-icon {
- margin-left: auto;
- }
-
- .ide-new-btn {
- display: none;
- margin-bottom: -4px;
- margin-right: -8px;
- }
-
- &:hover {
- .ide-new-btn {
- display: block;
- }
- }
-
- &.folder {
- svg {
- fill: $gl-text-color-secondary;
- }
- }
- }
-
- a {
- color: $gl-text-color;
- }
-
- th {
- position: sticky;
- top: 0;
- }
-}
-
-.file-name,
-.file-col-commit-message {
- display: flex;
- overflow: visible;
- padding: 6px 12px;
-}
-
-.multi-file-loading-container {
- margin-top: 10px;
- padding: 10px;
-
- .animation-container {
- background: $gray-light;
-
- div {
- background: $gray-light;
- }
- }
-}
-
-.multi-file-table-col-commit-message {
- white-space: nowrap;
- width: 50%;
-}
-
-.multi-file-edit-pane {
- display: flex;
- flex-direction: column;
- flex: 1;
- border-left: 1px solid $white-dark;
- overflow: hidden;
-}
-
-.multi-file-tabs {
- display: flex;
- background-color: $white-normal;
- box-shadow: inset 0 -1px $white-dark;
-
- > ul {
- display: flex;
- overflow-x: auto;
- }
-
- li {
- position: relative;
- }
-
- .dropdown {
- display: flex;
- margin-left: auto;
- margin-bottom: 1px;
- padding: 0 $grid-size;
- border-left: 1px solid $white-dark;
- background-color: $white-light;
-
- &.shadow {
- box-shadow: 0 0 10px $dropdown-shadow-color;
- }
-
- .btn {
- margin-top: auto;
- margin-bottom: auto;
- }
- }
-}
-
-.multi-file-tab {
- @include str-truncated(150px);
- padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
- background-color: $gray-normal;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- cursor: pointer;
-
- svg {
- vertical-align: middle;
- }
-
- &.active {
- background-color: $white-light;
- border-bottom-color: $white-light;
- }
-}
-
-.multi-file-tab-close {
- position: absolute;
- right: 8px;
- top: 50%;
- width: 16px;
- height: 16px;
- padding: 0;
- background: none;
- border: 0;
- border-radius: $border-radius-default;
- color: $theme-gray-900;
- transform: translateY(-50%);
-
- svg {
- position: relative;
- top: -1px;
- }
-
- &:hover {
- background-color: $theme-gray-200;
- }
-
- &:focus {
- background-color: $blue-500;
- color: $white-light;
- outline: 0;
-
- svg {
- fill: currentColor;
- }
- }
-}
-
-.multi-file-edit-pane-content {
- flex: 1;
- height: 0;
-}
-
-.blob-editor-container {
- flex: 1;
- height: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
-
- .vertical-center {
- min-height: auto;
- }
-
- .monaco-editor .lines-content .cigr {
- display: none;
- }
-
- .monaco-diff-editor.vs {
- .editor.modified {
- box-shadow: none;
- }
-
- .diagonal-fill {
- display: none !important;
- }
-
- .diffOverview {
- background-color: $white-light;
- border-left: 1px solid $white-dark;
- cursor: ns-resize;
- }
-
- .diffViewport {
- display: none;
- }
-
- .char-insert {
- background-color: $line-added-dark;
- }
-
- .char-delete {
- background-color: $line-removed-dark;
- }
-
- .line-numbers {
- color: $black-transparent;
- }
-
- .view-overlays {
- .line-insert {
- background-color: $line-added;
- }
-
- .line-delete {
- background-color: $line-removed;
- }
- }
-
- .margin {
- background-color: $gray-light;
- border-right: 1px solid $white-normal;
-
- .line-insert {
- border-right: 1px solid $line-added-dark;
- }
-
- .line-delete {
- border-right: 1px solid $line-removed-dark;
- }
- }
-
- .margin-view-overlays .insert-sign,
- .margin-view-overlays .delete-sign {
- opacity: 0.4;
- }
-
- .cursors-layer {
- display: none;
- }
- }
-}
-
-.multi-file-editor-holder {
- height: 100%;
-}
-
-.multi-file-editor-btn-group {
- padding: $gl-bar-padding $gl-padding;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
- background: $white-light;
-}
-
-.ide-status-bar {
- padding: $gl-bar-padding $gl-padding;
- background: $white-light;
- display: flex;
- justify-content: space-between;
-
- svg {
- vertical-align: middle;
- }
-}
-
-// Not great, but this is to deal with our current output
-.multi-file-preview-holder {
- height: 100%;
- overflow: scroll;
-
- .file-content.code {
- display: flex;
-
- i {
- margin-left: -10px;
- }
- }
-
- .line-numbers {
- min-width: 50px;
- }
-
- .file-content,
- .line-numbers,
- .blob-content,
- .code {
- min-height: 100%;
- }
-}
-
-.file-content.blob-no-preview {
- a {
- margin-left: auto;
- margin-right: auto;
- }
-}
-
-.multi-file-commit-panel {
- display: flex;
- position: relative;
- flex-direction: column;
- width: 340px;
- padding: 0;
- background-color: $gray-light;
- padding-right: 3px;
-
- .projects-sidebar {
- display: flex;
- flex-direction: column;
-
- .context-header {
- width: auto;
- margin-right: 0;
- }
- }
-
- .multi-file-commit-panel-inner {
- display: flex;
- flex: 1;
- flex-direction: column;
- }
-
- .multi-file-commit-panel-inner-scroll {
- display: flex;
- flex: 1;
- flex-direction: column;
- overflow: auto;
- }
-
- &.is-collapsed {
- width: 60px;
-
- .multi-file-commit-list {
- padding-top: $gl-padding;
- overflow: hidden;
- }
-
- .multi-file-context-bar-icon {
- align-items: center;
-
- svg {
- float: none;
- margin: 0;
- }
- }
- }
-
- .branch-container {
- border-left: 4px solid $indigo-700;
- margin-bottom: $gl-bar-padding;
- }
-
- .branch-header {
- background: $white-dark;
- display: flex;
- }
-
- .branch-header-title {
- flex: 1;
- padding: $grid-size $gl-padding;
- color: $indigo-700;
- font-weight: $gl-font-weight-bold;
-
- svg {
- vertical-align: middle;
- }
- }
-
- .branch-header-btns {
- padding: $gl-vert-padding $gl-padding;
- }
-
- .left-collapse-btn {
- display: none;
- background: $gray-light;
- text-align: left;
- border-top: 1px solid $white-dark;
-
- svg {
- vertical-align: middle;
- }
- }
-}
-
-.multi-file-context-bar-icon {
- padding: 10px;
-
- svg {
- margin-right: 10px;
- float: left;
- }
-}
-
-.multi-file-commit-panel-section {
- display: flex;
- flex-direction: column;
- flex: 1;
-}
-
-.multi-file-commit-empty-state-container {
- align-items: center;
- justify-content: center;
-}
-
-.multi-file-commit-panel-header {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- border-bottom: 1px solid $white-dark;
- padding: $gl-btn-padding 0;
-
- &.is-collapsed {
- border-bottom: 1px solid $white-dark;
-
- svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .multi-file-commit-panel-collapse-btn {
- margin-right: auto;
- margin-left: auto;
- border-left: 0;
- }
- }
-}
-
-.multi-file-commit-panel-header-title {
- display: flex;
- flex: 1;
- padding: 0 $gl-btn-padding;
-
- svg {
- margin-right: $gl-btn-padding;
- }
-}
-
-.multi-file-commit-panel-collapse-btn {
- border-left: 1px solid $white-dark;
-}
-
-.multi-file-commit-list {
- flex: 1;
- overflow: auto;
- padding: $gl-padding 0;
- min-height: 60px;
-}
-
-.multi-file-commit-list-item {
- display: flex;
- padding: 0;
- align-items: center;
-
- .multi-file-discard-btn {
- display: none;
- margin-left: auto;
- color: $gl-link-color;
- padding: 0 2px;
-
- &:focus,
- &:hover {
- text-decoration: underline;
- }
- }
-
- &:hover {
- background: $white-normal;
-
- .multi-file-discard-btn {
- display: block;
- }
- }
-}
-
-.multi-file-addition {
- fill: $green-500;
-}
-
-.multi-file-modified {
- fill: $orange-500;
-}
-
-.multi-file-commit-list-collapsed {
- display: flex;
- flex-direction: column;
-
- > svg {
- margin-left: auto;
- margin-right: auto;
- }
-
- .file-status-icon {
- width: 10px;
- height: 10px;
- margin-left: 3px;
- }
-}
-
-.multi-file-commit-list-path {
- padding: $grid-size / 2;
- padding-left: $gl-padding;
- background: none;
- border: 0;
- text-align: left;
- width: 100%;
- min-width: 0;
-
- svg {
- min-width: 16px;
- vertical-align: middle;
- display: inline-block;
- }
-
- &:hover,
- &:focus {
- outline: 0;
- }
-}
-
-.multi-file-commit-list-file-path {
- @include str-truncated(100%);
-
- &:hover {
- text-decoration: underline;
- }
-
- &:active {
- text-decoration: none;
- }
-}
-
-.multi-file-commit-form {
- padding: $gl-padding;
- border-top: 1px solid $white-dark;
-
- .btn {
- font-size: $gl-font-size;
- }
-}
-
-.multi-file-commit-message.form-control {
- height: 160px;
- resize: none;
-}
-
-.dirty-diff {
- // !important need to override monaco inline style
- width: 4px !important;
- left: 0 !important;
-
- &-modified {
- background-color: $blue-500;
- }
-
- &-added {
- background-color: $green-600;
- }
-
- &-removed {
- height: 0 !important;
- width: 0 !important;
- bottom: -2px;
- border-style: solid;
- border-width: 5px;
- border-color: transparent transparent transparent $red-500;
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- width: 100px;
- height: 1px;
- background-color: rgba($red-500, 0.5);
- }
- }
-}
-
-.ide-loading {
- display: flex;
- height: 100vh;
- align-items: center;
- justify-content: center;
-}
-
-.ide-empty-state {
- display: flex;
- height: 100vh;
- align-items: center;
- justify-content: center;
-}
-
-.ide-new-btn {
- .dropdown-toggle svg {
- margin-top: -2px;
- margin-bottom: 2px;
- }
-
- .dropdown-menu {
- left: auto;
- right: 0;
-
- label {
- font-weight: $gl-font-weight-normal;
- padding: 5px 8px;
- margin-bottom: 0;
- }
- }
-}
-
-.ide {
- overflow: hidden;
-
- &.nav-only {
- .flash-container {
- margin-top: $header-height;
- margin-bottom: 0;
- }
-
- .alert-wrapper .flash-container .flash-alert:last-child,
- .alert-wrapper .flash-container .flash-notice:last-child {
- margin-bottom: 0;
- }
-
- .content-wrapper {
- margin-top: $header-height;
- padding-bottom: 0;
- }
-
- &.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
- .ide-view {
- height: calc(100vh - #{$header-height + $flash-height});
- }
- }
-
- .projects-sidebar {
- .multi-file-commit-panel-inner-scroll {
- flex: 1;
- }
- }
- }
-}
-
-.with-performance-bar .ide.nav-only {
- .flash-container {
- margin-top: #{$header-height + $performance-bar-height};
- }
-
- .content-wrapper {
- margin-top: #{$header-height + $performance-bar-height};
- padding-bottom: 0;
- }
-
- .ide-view {
- height: calc(100vh - #{$header-height + $performance-bar-height});
- }
-
- &.flash-shown {
- .content-wrapper {
- margin-top: 0;
- }
-
- .ide-view {
- height: calc(
- 100vh - #{$header-height + $performance-bar-height + $flash-height}
- );
- }
- }
-}
-
-.dragHandle {
- position: absolute;
- top: 0;
- bottom: 0;
- width: 3px;
- background-color: $white-dark;
-
- &.dragright {
- right: 0;
- }
-
- &.dragleft {
- left: 0;
- }
-}
-
-.ide-commit-radios {
- label {
- font-weight: normal;
- }
-
- .help-block {
- margin-top: 0;
- line-height: 0;
- }
-}
-
-.ide-commit-new-branch {
- margin-left: 25px;
-}
-
-.ide-external-links {
- p {
- margin: 0;
- }
-}
-
-.ide-sidebar-link {
- padding: $gl-padding-8 $gl-padding;
- background: $indigo-700;
- color: $white-light;
- text-decoration: none;
- display: flex;
- align-items: center;
-
- &:focus,
- &:hover {
- color: $white-light;
- text-decoration: underline;
- background: $indigo-500;
- }
-
- &:active {
- background: $indigo-800;
- }
-}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 5fb97b13470..2734faec558 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -51,7 +51,7 @@
}
}
-@media (max-width: $screen-md-max) {
+@include media-breakpoint-down(md) {
.runners-content {
width: 100%;
overflow: auto;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index dbde0720993..a35c4ff7c80 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -47,6 +47,7 @@ input[type="checkbox"]:hover {
}
.location-badge {
+ white-space: nowrap;
height: 32px;
font-size: 12px;
margin: -4px 4px -4px -4px;
@@ -166,7 +167,7 @@ input[type="checkbox"]:hover {
}
.search-holder {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: -webkit-flex;
display: flex;
}
@@ -178,7 +179,7 @@ input[type="checkbox"]:hover {
position: relative;
margin-right: 0;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-right: 5px;
}
}
@@ -202,7 +203,7 @@ input[type="checkbox"]:hover {
width: 100%;
margin-top: 5px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: auto;
margin-top: 0;
margin-left: 5px;
@@ -210,7 +211,7 @@ input[type="checkbox"]:hover {
}
.dropdown {
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
margin-left: 5px;
margin-right: 5px;
}
@@ -220,7 +221,7 @@ input[type="checkbox"]:hover {
width: 100%;
margin-top: 5px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 180px;
margin-top: 0;
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index c410049bc0b..2b3773eebad 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -70,7 +70,7 @@
animation: none;
}
- @media(max-width: $screen-sm-max) {
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
padding-right: 20px;
}
@@ -98,8 +98,8 @@
}
.bs-callout,
- .checkbox:first-child,
- .help-block {
+ .form-check:first-child,
+ .form-text.text-muted {
margin-top: 0;
}
@@ -131,12 +131,12 @@
color: $gl-danger;
}
-.service-settings .control-label {
+.service-settings .form-control-label {
padding-top: 0;
}
.integration-settings-form {
- .well {
+ .card.card-body {
padding: $gl-padding / 2;
box-shadow: none;
}
@@ -158,7 +158,7 @@
}
.visibility-level-setting {
- .radio {
+ .form-check {
margin-bottom: 10px;
i.fa {
@@ -174,7 +174,7 @@
.option-description,
.option-disabled-reason {
- margin-left: 29px;
+ margin-left: 45px;
color: $project-option-descr-color;
}
@@ -199,22 +199,22 @@
}
.prometheus-metrics-monitoring {
- .panel {
- .panel-toggle {
+ .card {
+ .card-toggle {
width: 14px;
}
- .badge {
+ .badge.badge-pill {
font-size: 12px;
line-height: 12px;
}
- .panel-heading .badge-count {
+ .card-header .label-count {
color: $white-light;
background: $common-gray-dark;
}
- .panel-body {
+ .card-body {
padding: 0;
}
@@ -249,7 +249,7 @@
li {
padding: $gl-padding;
- .badge {
+ .badge.badge-pill {
margin-left: 5px;
background: $badge-bg;
}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 8e2c42c1bd3..3f6f5f06075 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -14,7 +14,10 @@
}
#contributors-master {
- @include make-md-column(12);
+ @include media-breakpoint-up(md) {
+ @include make-col-ready();
+ @include make-col(12);
+ }
svg {
width: 100%;
@@ -33,10 +36,14 @@
}
.person {
- @include make-md-column(6);
+ @include media-breakpoint-up(md) {
+ @include make-col-ready();
+ @include make-col(6);
+ }
+
margin-top: 10px;
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
width: 100%;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index ade5ddd147b..620297e589d 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -58,7 +58,7 @@
}
}
-.visible-xs-inline {
+.d-block.d-sm-none-inline {
.ci-status-link {
position: relative;
top: 2px;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 4b9824fab0c..e5d7dd13915 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -126,7 +126,7 @@
color: $gl-grayish-blue;
font-size: $gl-font-size;
- .label {
+ .badge.badge-pill {
color: $gl-text-color;
}
@@ -162,7 +162,7 @@
}
}
-@media (max-width: $screen-sm-max) {
+@include media-breakpoint-down(sm) {
.todos-filters {
.dropdown-menu-toggle {
width: 130px;
@@ -174,7 +174,7 @@
}
}
-@media (max-width: $screen-xs-max) {
+@include media-breakpoint-down(xs) {
.todo {
.avatar {
display: none;
@@ -214,7 +214,7 @@
margin-left: auto;
margin-right: auto;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
-webkit-flex-direction: row;
flex-direction: row;
padding-top: 80px;
@@ -233,7 +233,7 @@
margin-left: auto;
margin-right: auto;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
width: 300px;
margin-right: 0;
-webkit-order: 2;
@@ -244,7 +244,7 @@
.todos-all-done {
padding-top: 20px;
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
padding-top: 50px;
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index e0ee7e9aa3d..efd26cb1f81 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -7,7 +7,7 @@
color: $gl-text-color-secondary;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
display: flex;
.tree-ref-container {
@@ -41,15 +41,10 @@
position: relative;
}
}
-
- .add-to-tree-dropdown {
- position: absolute;
- left: 18px;
- }
}
}
- @media (max-width: $screen-xs-max) {
+ @include media-breakpoint-down(xs) {
.repo-breadcrumb {
margin-top: 10px;
position: relative;
@@ -121,7 +116,7 @@
margin-left: 5px;
}
- @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
+ @include media-breakpoint-only(md) {
@include str-truncated(450px);
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 9a0ec936979..800f5c68e39 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -65,7 +65,7 @@
display: block;
}
- @media (min-width: $screen-sm-min) {
+ @include media-breakpoint-up(sm) {
&.has-sidebar-toggle {
padding-right: 40px;
}
@@ -81,7 +81,7 @@
}
}
- @media (min-width: $screen-md-min) {
+ @include media-breakpoint-up(md) {
&.has-sidebar-toggle {
padding-right: 0;
}
@@ -180,11 +180,6 @@ ul.wiki-pages-list.content-list {
}
}
-.wiki-holder {
- overflow-x: auto;
- overflow-y: hidden;
-}
-
.wiki {
table {
@include markdown-table;
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 45ae94abaff..06ef58531d7 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -1,5 +1,4 @@
@import 'framework/variables';
-@import 'peek/views/performance_bar';
@import 'peek/views/rblineprof';
#js-peek {
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index b07a5ae22cd..90ccd4abd90 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -36,7 +36,9 @@ ul.notes-form,
.gutter-toggle,
.issuable-details .content-block-small,
.edit-link,
-.note-action-button {
+.note-action-button,
+.right-sidebar,
+.flash-container {
display: none !important;
}
@@ -53,3 +55,7 @@ pre {
.right-sidebar {
top: 0;
}
+
+a[href]::after {
+ content: none !important;
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8958eab0423..cdfe3d6ab1e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -52,7 +52,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private
def set_application_setting
- @application_setting = ApplicationSetting.current
+ @application_setting = ApplicationSetting.current_without_cache
end
def application_setting_params
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index e85cdcb8db7..737942f3eb2 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,5 +1,11 @@
class Admin::DashboardController < Admin::ApplicationController
+ include CountHelper
+
+ COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest,
+ Note, Snippet, Key, Milestone].freeze
+
def index
+ @counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 0fdd4d2cb47..2843d70c645 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -13,12 +13,13 @@ class ApplicationController < ActionController::Base
before_action :authenticate_sessionless_user!
before_action :authenticate_user!
+ before_action :enforce_terms!, if: :should_enforce_terms?
before_action :validate_user_service_ticket!
before_action :check_password_expiration
before_action :ldap_security_check
before_action :sentry_context
before_action :default_headers
- before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') }
+ before_action :add_gon_variables, unless: :peek_request?
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
@@ -110,7 +111,8 @@ class ApplicationController < ActionController::Base
def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled?
- application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
+ backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
+ application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
@@ -268,6 +270,27 @@ class ApplicationController < ActionController::Base
end
end
+ def enforce_terms!
+ return unless current_user
+ return if current_user.terms_accepted?
+
+ if sessionless_user?
+ render_403
+ else
+ # Redirect to the destination if the request is a get.
+ # Redirect to the source if it was a post, so the user can re-submit after
+ # accepting the terms.
+ redirect_path = if request.get?
+ request.fullpath
+ else
+ URI(request.referer).path if request.referer
+ end
+
+ flash[:notice] = _("Please accept the Terms of Service before continuing.")
+ redirect_to terms_path(redirect: redirect_path), status: :found
+ end
+ end
+
def import_sources_enabled?
!Gitlab::CurrentSettings.import_sources.empty?
end
@@ -341,4 +364,18 @@ class ApplicationController < ActionController::Base
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
+
+ def sessionless_user?
+ current_user && !session.keys.include?('warden.user.user.key')
+ end
+
+ def peek_request?
+ request.path.start_with?('/-/peek')
+ end
+
+ def should_enforce_terms?
+ return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms
+
+ !(peek_request? || devise_controller?)
+ end
end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 7d7ff217e5d..09e143c23e8 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -94,7 +94,7 @@ module Boards
def serialize_as_json(resource)
resource.as_json(
- only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
+ only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight],
labels: true,
issue_endpoints: true,
include_full_project_path: board.group_board?,
diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb
new file mode 100644
index 00000000000..6e8aef52b52
--- /dev/null
+++ b/app/controllers/concerns/accepts_pending_invitations.rb
@@ -0,0 +1,15 @@
+module AcceptsPendingInvitations
+ extend ActiveSupport::Concern
+
+ def accept_pending_invitations
+ return unless resource.active_for_authentication?
+
+ clear_stored_location_for_resource if resource.accept_pending_invitations!.any?
+ end
+
+ def clear_stored_location_for_resource
+ session_key = stored_location_key_for(resource)
+
+ session.delete(session_key)
+ end
+end
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 2fdf346ef44..69a053d4246 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
+ # Set @user for Devise views
+ @user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index eb3a623acdd..8b7355974df 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -1,4 +1,5 @@
module ContinueParams
+ include InternalRedirect
extend ActiveSupport::Concern
def continue_params
@@ -6,8 +7,7 @@ module ContinueParams
return nil unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
- return unless continue_params[:to] && continue_params[:to].start_with?('/')
- return if continue_params[:to].start_with?('//')
+ continue_params[:to] = safe_redirect_path(continue_params[:to])
continue_params
end
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
new file mode 100644
index 00000000000..7409b2e89a5
--- /dev/null
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -0,0 +1,35 @@
+module InternalRedirect
+ extend ActiveSupport::Concern
+
+ def safe_redirect_path(path)
+ return unless path
+ # Verify that the string starts with a `/` but not a double `/`.
+ return unless path =~ %r{^/\w.*$}
+
+ uri = URI(path)
+ # Ignore anything path of the redirect except for the path, querystring and,
+ # fragment, forcing the redirect within the same host.
+ full_path_for_uri(uri)
+ rescue URI::InvalidURIError
+ nil
+ end
+
+ def safe_redirect_path_for_url(url)
+ return unless url
+
+ uri = URI(url)
+ safe_redirect_path(full_path_for_uri(uri)) if host_allowed?(uri)
+ rescue URI::InvalidURIError
+ nil
+ end
+
+ def host_allowed?(uri)
+ uri.host == request.host &&
+ uri.port == request.port
+ end
+
+ def full_path_for_uri(uri)
+ path_with_query = [uri.path, uri.query].compact.join('?')
+ [path_with_query, uri.fragment].compact.join("#")
+ end
+end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0379f76fc3d..c925b4aada5 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -18,7 +18,6 @@ module IssuableActions
def update
@issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
respond_to do |format|
format.html do
recaptcha_check_if_spammable { render :edit }
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 4114ca6bf7c..ca1b80a36a0 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -57,7 +57,7 @@ module IssuableCollections
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range
- redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
+ redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
end
out_of_range
@@ -165,8 +165,8 @@ module IssuableCollections
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
when 'MergeRequest'
[
- :source_project, :target_project, :author, :assignee, :labels, :milestone,
- head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
+ :target_project, :author, :assignee, :labels, :milestone,
+ source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
end
end
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 55011c89886..237c93daee8 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -2,6 +2,10 @@ module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment')
if attachment
redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" }
+ # By default, Rails will send uploads with an extension of .js with a
+ # content-type of text/javascript, which will trigger Rails'
+ # cross-origin JavaScript protection.
+ send_params[:content_type] = 'text/plain' if File.extname(attachment) == '.js'
send_params.merge!(filename: attachment, disposition: disposition)
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 6d9c38d9581..7bc46a6ccc0 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -1,4 +1,6 @@
class ConfirmationsController < Devise::ConfirmationsController
+ include AcceptsPendingInvitations
+
def almost_there
flash[:notice] = nil
render layout: "devise_empty"
@@ -11,6 +13,8 @@ class ConfirmationsController < Devise::ConfirmationsController
end
def after_confirmation_path_for(resource_name, resource)
+ accept_pending_invitations
+
# incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource)
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 9f3bb60b4cc..62213561898 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group)
params[:group_id] = group.to_param
- url_for(params)
+ url_for(safe_params)
end
end
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 7c2016f0326..e892d1f8dbf 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -2,19 +2,24 @@ class Groups::BoardsController < Groups::ApplicationController
include BoardsResponses
before_action :assign_endpoint_vars
+ before_action :boards, only: :index
def index
- @boards = Boards::ListService.new(group, current_user).execute
-
respond_with_boards
end
def show
- @board = group.boards.find(params[:id])
+ @board = boards.find(params[:id])
respond_with_board
end
+ private
+
+ def boards
+ @boards ||= Boards::ListService.new(group, current_user).execute
+ end
+
def assign_endpoint_vars
@boards_endpoint = group_boards_url(group)
@namespace_path = group.to_param
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 134b0dfc0db..ef3eba80154 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -11,13 +11,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override
def index
+ can_manage_members = can?(current_user, :admin_group_member, @group)
+
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = GroupMembersFinder.new(@group).execute
- @members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.non_invite unless can_manage_members
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort_by_attribute(@sort)
+
+ if can_manage_members && params[:two_factor].present?
+ @members = @members.filter_by_2fa(params[:two_factor])
+ end
+
@members = @members.page(params[:page]).per(50)
@members = present_members(@members.includes(:user))
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
new file mode 100644
index 00000000000..78992ec7f46
--- /dev/null
+++ b/app/controllers/groups/runners_controller.rb
@@ -0,0 +1,58 @@
+class Groups::RunnersController < Groups::ApplicationController
+ # Proper policies should be implemented per
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
+ before_action :authorize_admin_pipeline!
+
+ before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+
+ def show
+ render 'shared/runners/show'
+ end
+
+ def edit
+ end
+
+ def update
+ if Ci::UpdateRunnerService.new(@runner).update(runner_params)
+ redirect_to group_runner_path(@group, @runner), notice: 'Runner was successfully updated.'
+ else
+ render 'edit'
+ end
+ end
+
+ def destroy
+ @runner.destroy
+
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302
+ end
+
+ def resume
+ if Ci::UpdateRunnerService.new(@runner).update(active: true)
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
+ else
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if Ci::UpdateRunnerService.new(@runner).update(active: false)
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), notice: 'Runner was successfully updated.'
+ else
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), alert: 'Runner was not updated.'
+ end
+ end
+
+ private
+
+ def runner
+ @runner ||= @group.runners.find(params[:id])
+ end
+
+ def authorize_admin_pipeline!
+ return render_404 unless can?(current_user, :admin_pipeline, group)
+ end
+
+ def runner_params
+ params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
+ end
+end
diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb
index edb334a3d88..ccbd0a3bc02 100644
--- a/app/controllers/groups/settings/badges_controller.rb
+++ b/app/controllers/groups/settings/badges_controller.rb
@@ -1,12 +1,12 @@
module Groups
module Settings
class BadgesController < Groups::ApplicationController
- include GrapeRouteHelpers::NamedRouteMatcher
+ include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_group!
def index
- @badge_api_endpoint = api_v4_groups_badges_path(id: @group.id)
+ @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id))
end
end
end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index c84fc2d305d..663269a0f92 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,6 +1,17 @@
class Import::BaseController < ApplicationController
private
+ def find_already_added_projects(import_type)
+ current_user.created_projects.where(import_type: import_type).includes(:import_state)
+ end
+
+ def find_jobs(import_type)
+ current_user.created_projects
+ .includes(:import_state)
+ .where(import_type: import_type)
+ .to_json(only: [:id], methods: [:import_status])
+ end
+
def find_or_create_namespace(names, owner)
names = params[:target_namespace].presence || names
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 61d81ad8a71..77af5fb9c4f 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -22,16 +22,14 @@ class Import::BitbucketController < Import::BaseController
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
- @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
+ @already_added_projects = find_already_added_projects('bitbucket')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
end
def jobs
- render json: current_user.created_projects
- .where(import_type: 'bitbucket')
- .to_json(only: [:id, :import_status])
+ render json: find_jobs('bitbucket')
end
def create
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 669eb31a995..25ec13b8075 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -46,15 +46,14 @@ class Import::FogbugzController < Import::BaseController
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: 'fogbugz')
+ @already_added_projects = find_already_added_projects('fogbugz')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('fogbugz')
end
def create
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index eb7d5fca367..f67ec4c248b 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -24,15 +24,14 @@ class Import::GithubController < Import::BaseController
def status
@repos = client.repos
- @already_added_projects = current_user.created_projects.where(import_type: provider)
+ @already_added_projects = find_already_added_projects(provider)
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs(provider)
end
def create
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 18f1d20f5a9..39e2e9e094b 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -12,15 +12,14 @@ class Import::GitlabController < Import::BaseController
def status
@repos = client.projects
- @already_added_projects = current_user.created_projects.where(import_type: "gitlab")
+ @already_added_projects = find_already_added_projects('gitlab')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "gitlab").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('gitlab')
end
def create
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index baa19fb383d..9b26a00f7c7 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -73,15 +73,14 @@ class Import::GoogleCodeController < Import::BaseController
@repos = client.repos
@incompatible_repos = client.incompatible_repos
- @already_added_projects = current_user.created_projects.where(import_type: "google_code")
+ @already_added_projects = find_already_added_projects('google_code')
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include? repo.name }
end
def jobs
- jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status])
- render json: jobs
+ render json: find_jobs('google_code')
end
def create
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
new file mode 100644
index 00000000000..fb24edb8602
--- /dev/null
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -0,0 +1,31 @@
+class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
+ extend ::Gitlab::Utils::Override
+
+ def self.define_providers!
+ return unless Gitlab::Auth::LDAP::Config.enabled?
+
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
+ alias_method server['provider_name'], :ldap
+ end
+ end
+
+ # We only find ourselves here
+ # if the authentication to LDAP was successful.
+ def ldap
+ sign_in_user_flow(Gitlab::Auth::LDAP::User)
+ end
+
+ define_providers!
+
+ override :set_remember_me
+ def set_remember_me(user)
+ user.remember_me = params[:remember_me] if user.persisted?
+ end
+
+ override :fail_login
+ def fail_login(user)
+ flash[:alert] = 'Access denied for your LDAP account.'
+
+ redirect_to new_user_session_path
+ end
+end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 5e6676ea513..27fd5f7ba37 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:kerberos, :saml, :cas3]
- Gitlab.config.omniauth.providers.each do |provider|
- define_method provider['name'] do
- handle_omniauth
- end
+ def handle_omniauth
+ omniauth_flow(Gitlab::Auth::OAuth)
end
- if Gitlab::Auth::LDAP::Config.enabled?
- Gitlab::Auth::LDAP::Config.available_servers.each do |server|
- define_method server['provider_name'] do
- ldap
- end
- end
+ AuthHelper.providers_for_base_controller.each do |provider|
+ alias_method provider, :handle_omniauth
end
# Extend the standard implementation to also increment
@@ -32,56 +26,17 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Extend the standard message generation to accept our custom exception
def failure_message
- exception = env["omniauth.error"]
+ exception = request.env["omniauth.error"]
error = exception.error_reason if exception.respond_to?(:error_reason)
error ||= exception.error if exception.respond_to?(:error)
error ||= exception.message if exception.respond_to?(:message)
- error ||= env["omniauth.error.type"].to_s
- error.to_s.humanize if error
- end
+ error ||= request.env["omniauth.error.type"].to_s
- # We only find ourselves here
- # if the authentication to LDAP was successful.
- def ldap
- ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
- ldap_user.save if ldap_user.changed? # will also save new users
-
- @user = ldap_user.gl_user
- @user.remember_me = params[:remember_me] if ldap_user.persisted?
-
- # Do additional LDAP checks for the user filter and EE features
- if ldap_user.allowed?
- if @user.two_factor_enabled?
- prompt_for_two_factor(@user)
- else
- log_audit_event(@user, with: oauth['provider'])
- sign_in_and_redirect(@user)
- end
- else
- fail_ldap_login
- end
+ error.to_s.humanize if error
end
def saml
- if current_user
- log_audit_event(current_user, with: :saml)
- # Update SAML identity if data has changed.
- identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
- if identity.nil?
- current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
- redirect_to profile_account_path, notice: 'Authentication method updated'
- else
- redirect_to after_sign_in_path_for(current_user)
- end
- else
- saml_user = Gitlab::Auth::Saml::User.new(oauth)
- saml_user.save if saml_user.changed?
- @user = saml_user.gl_user
-
- continue_login_process
- end
- rescue Gitlab::Auth::OAuth::User::SignupDisabledError
- handle_signup_error
+ omniauth_flow(Gitlab::Auth::Saml)
end
def omniauth_error
@@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
- def handle_omniauth
+ def omniauth_flow(auth_module, identity_linker: nil)
if current_user
- # Add new authentication method
- current_user.identities
- .with_extern_uid(oauth['provider'], oauth['uid'])
- .first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider'])
- redirect_to profile_account_path, notice: 'Authentication method updated'
- else
- oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
- oauth_user.save
- @user = oauth_user.gl_user
- continue_login_process
+ identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
+
+ identity_linker.link
+
+ if identity_linker.changed?
+ redirect_identity_linked
+ elsif identity_linker.failed?
+ redirect_identity_link_failed(identity_linker.error_message)
+ else
+ redirect_identity_exists
+ end
+ else
+ sign_in_user_flow(auth_module::User)
end
- rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
- handle_disabled_provider
- rescue Gitlab::Auth::OAuth::User::SignupDisabledError
- handle_signup_error
+ end
+
+ def redirect_identity_exists
+ redirect_to after_sign_in_path_for(current_user)
+ end
+
+ def redirect_identity_link_failed(error_message)
+ redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
+ end
+
+ def redirect_identity_linked
+ redirect_to profile_account_path, notice: 'Authentication method updated'
end
def handle_service_ticket(provider, ticket)
@@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket
end
- def continue_login_process
- # Only allow properly saved users to login.
- if @user.persisted? && @user.valid?
- log_audit_event(@user, with: oauth['provider'])
+ def sign_in_user_flow(auth_user_class)
+ auth_user = auth_user_class.new(oauth)
+ user = auth_user.find_and_update!
+
+ if auth_user.valid_sign_in?
+ log_audit_event(user, with: oauth['provider'])
+
+ set_remember_me(user)
- if @user.two_factor_enabled?
- params[:remember_me] = '1' if remember_me?
- prompt_for_two_factor(@user)
+ if user.two_factor_enabled?
+ prompt_for_two_factor(user)
else
- remember_me(@user) if remember_me?
- sign_in_and_redirect(@user)
+ sign_in_and_redirect(user)
end
else
- fail_login
+ fail_login(user)
end
+ rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
+ handle_disabled_provider
+ rescue Gitlab::Auth::OAuth::User::SignupDisabledError
+ handle_signup_error
end
def handle_signup_error
@@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
@oauth ||= request.env['omniauth.auth']
end
- def fail_login
- error_message = @user.errors.full_messages.to_sentence
+ def fail_login(user)
+ error_message = user.errors.full_messages.to_sentence
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end
- def fail_ldap_login
- flash[:alert] = 'Access denied for your LDAP account.'
-
- redirect_to new_user_session_path
- end
-
def fail_auth0_login
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
@@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
.for_authentication.security_event
end
+ def set_remember_me(user)
+ return unless remember_me?
+
+ if user.two_factor_enabled?
+ params[:remember_me] = '1'
+ else
+ remember_me(user)
+ end
+ end
+
def remember_me?
request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present?
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
new file mode 100644
index 00000000000..f0cdc228366
--- /dev/null
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -0,0 +1,14 @@
+class Profiles::ActiveSessionsController < Profiles::ApplicationController
+ def index
+ @sessions = ActiveSession.list(current_user)
+ end
+
+ def destroy
+ ActiveSession.destroy(current_user, params[:id])
+
+ respond_to do |format|
+ format.html { redirect_to profile_active_sessions_url, status: 302 }
+ format.js { head :ok }
+ end
+ end
+end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f0e5d2aa94e..12a6cd11f80 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -23,7 +23,7 @@ class Profiles::KeysController < Profiles::ApplicationController
def destroy
@key = current_user.keys.find(params[:id])
- @key.destroy
+ Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format|
format.html { redirect_to profile_keys_url, status: 302 }
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 032bb2267e7..5ab6d103c89 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param
- url_for(params)
+ url_for(safe_params)
end
def repository
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 949e54ff819..e7354a9e1f7 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -4,22 +4,25 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show]
+ before_action :boards, only: :index
before_action :assign_endpoint_vars
def index
- @boards = Boards::ListService.new(project, current_user).execute
-
respond_with_boards
end
def show
- @board = project.boards.find(params[:id])
+ @board = boards.find(params[:id])
respond_with_board
end
private
+ def boards
+ @boards ||= Boards::ListService.new(project, current_user).execute
+ end
+
def assign_endpoint_vars
@boards_endpoint = project_boards_path(project)
@bulk_issues_path = bulk_update_project_issues_path(project)
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index 90c7fa62216..35885543622 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -5,9 +5,10 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
before_action :authorize_create_cluster!, only: [:create]
def create
- Clusters::Applications::ScheduleInstallationService.new(project, current_user,
- application_class: @application_class,
- cluster: @cluster).execute
+ application = @application_class.find_or_create_by!(cluster: @cluster)
+
+ Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application)
+
head :no_content
rescue StandardError
head :bad_request
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index b7f548e0e63..1d1184d46d1 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -23,8 +23,12 @@ class Projects::CommitController < Projects::ApplicationController
respond_to do |format|
format.html { render }
- format.diff { render text: @commit.to_diff }
- format.patch { render text: @commit.to_patch }
+ format.diff do
+ send_git_diff(@project.repository, @commit.diff_refs)
+ end
+ format.patch do
+ send_git_patch(@project.repository, @commit.diff_refs)
+ end
end
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 2b0c2ca97c0..f93e500a07a 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -8,8 +8,11 @@ class Projects::CompareController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :define_ref_vars, only: [:index, :show, :diff_for_path]
- before_action :define_diff_vars, only: [:show, :diff_for_path]
+ # Defining ivars
+ before_action :define_diffs, only: [:show, :diff_for_path]
+ before_action :define_environment, only: [:show]
+ before_action :define_diff_notes_disabled, only: [:show, :diff_for_path]
+ before_action :define_commits, only: [:show, :diff_for_path, :signatures]
before_action :merge_request, only: [:index, :show]
def index
@@ -22,9 +25,9 @@ class Projects::CompareController < Projects::ApplicationController
end
def diff_for_path
- return render_404 unless @compare
+ return render_404 unless compare
- render_diff_for_path(@compare.diffs(diff_options))
+ render_diff_for_path(compare.diffs(diff_options))
end
def create
@@ -41,30 +44,60 @@ class Projects::CompareController < Projects::ApplicationController
end
end
+ def signatures
+ respond_to do |format|
+ format.json do
+ render json: {
+ signatures: @commits.select(&:has_signature?).map do |commit|
+ {
+ commit_sha: commit.sha,
+ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
+ }
+ end
+ }
+ end
+ end
+ end
+
private
- def define_ref_vars
- @start_ref = Addressable::URI.unescape(params[:from])
+ def compare
+ return @compare if defined?(@compare)
+
+ @compare = CompareService.new(@project, head_ref).execute(@project, start_ref)
+ end
+
+ def start_ref
+ @start_ref ||= Addressable::URI.unescape(params[:from])
+ end
+
+ def head_ref
+ return @ref if defined?(@ref)
+
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
- def define_diff_vars
- @compare = CompareService.new(@project, @head_ref)
- .execute(@project, @start_ref)
+ def define_commits
+ @commits = compare.present? ? prepare_commits_for_rendering(compare.commits) : []
+ end
- if @compare
- @commits = prepare_commits_for_rendering(@compare.commits)
- @diffs = @compare.diffs(diff_options)
+ def define_diffs
+ @diffs = compare.present? ? compare.diffs(diff_options) : []
+ end
- environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
+ def define_environment
+ if compare
+ environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
-
- @diff_notes_disabled = true
end
end
+ def define_diff_notes_disabled
+ @diff_notes_disabled = compare.present?
+ end
+
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
- .find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
+ .find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref)
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 767e492f566..d69015c8665 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch
can_create = current_user &&
can?(current_user, :push_code, @project) &&
- @issue.can_be_worked_on?(current_user)
+ @issue.can_be_worked_on?
respond_to do |format|
format.json do
- render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? }
+ render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
end
end
end
@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def authorize_create_merge_request!
- render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?
end
def render_issue_json
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index ebde0df1f7b..43d8867a536 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
- object.projects << storage_project
- object.save!
+ object.lfs_objects_projects.create!(project: storage_project)
end
end
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 4a377fefc62..81129456ad8 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
render layout: false
end
- def update_branches
- @target_project = selected_target_project
- @target_branches = @target_project ? @target_project.repository.branch_names : []
-
- render layout: false
- end
-
private
def build_merge_request
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
new file mode 100644
index 00000000000..5698ff4e706
--- /dev/null
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -0,0 +1,67 @@
+class Projects::MirrorsController < Projects::ApplicationController
+ include RepositorySettingsRedirect
+
+ # Authorize
+ before_action :remote_mirror, only: [:update]
+ before_action :check_mirror_available!
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def show
+ redirect_to_repository_settings(project)
+ end
+
+ def update
+ if project.update_attributes(mirror_params)
+ flash[:notice] = 'Mirroring settings were successfully updated.'
+ else
+ flash[:alert] = project.errors.full_messages.join(', ').html_safe
+ end
+
+ respond_to do |format|
+ format.html { redirect_to_repository_settings(project) }
+ format.json do
+ if project.errors.present?
+ render json: project.errors, status: :unprocessable_entity
+ else
+ render json: ProjectMirrorSerializer.new.represent(project)
+ end
+ end
+ end
+ end
+
+ def update_now
+ if params[:sync_remote]
+ project.update_remote_mirrors
+ flash[:notice] = "The remote repository is being updated..."
+ end
+
+ redirect_to_repository_settings(project)
+ end
+
+ private
+
+ def remote_mirror
+ @remote_mirror = project.remote_mirrors.first_or_initialize
+ end
+
+ def check_mirror_available!
+ Gitlab::CurrentSettings.current_application_settings.mirror_available || current_user&.admin?
+ end
+
+ def mirror_params_attributes
+ [
+ remote_mirrors_attributes: %i[
+ url
+ id
+ enabled
+ only_protected_branches
+ ]
+ ]
+ end
+
+ def mirror_params
+ params.require(:project).permit(mirror_params_attributes)
+ end
+end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 86c50d88a2a..4d4c2af2415 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -8,19 +8,6 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_create_note!, only: [:create]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
- #
- # This is a fix to make spinach feature tests passing:
- # Controller actions are returned from AbstractController::Base and methods of parent classes are
- # excluded in order to return only specific controller related methods.
- # That is ok for the app (no :create method in ancestors)
- # but fails for tests because there is a :create method on FactoryBot (one of the ancestors)
- #
- # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
- #
- def create
- super
- end
-
def delete_attachment
note.remove_attachment!
note.update_attribute(:attachment, nil)
@@ -33,9 +20,7 @@ class Projects::NotesController < Projects::ApplicationController
def resolve
return render_404 unless note.resolvable?
- note.resolve!(current_user)
-
- MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
+ Notes::ResolveService.new(project, current_user).execute(note)
discussion = note.discussion
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 78d109cf33e..6b40fc2fe68 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -18,19 +18,12 @@ class Projects::PipelinesController < Projects::ApplicationController
.page(params[:page])
.per(30)
- @running_count = PipelinesFinder
- .new(project, scope: 'running').execute.count
+ @running_count = limited_pipelines_count(project, 'running')
+ @pending_count = limited_pipelines_count(project, 'pending')
+ @finished_count = limited_pipelines_count(project, 'finished')
+ @pipelines_count = limited_pipelines_count(project)
- @pending_count = PipelinesFinder
- .new(project, scope: 'pending').execute.count
-
- @finished_count = PipelinesFinder
- .new(project, scope: 'finished').execute.count
-
- @pipelines_count = PipelinesFinder
- .new(project).execute.count
-
- @pipelines.map(&:commit) # List commits for batch loading
+ Gitlab::Ci::Pipeline::Preloader.preload(@pipelines)
respond_to do |format|
format.html
@@ -41,7 +34,7 @@ class Projects::PipelinesController < Projects::ApplicationController
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
- .represent(@pipelines),
+ .represent(@pipelines, disable_coverage: true),
count: {
all: @pipelines_count,
running: @running_count,
@@ -87,7 +80,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def failures
- if @pipeline.statuses.latest.failed.present?
+ if @pipeline.failed_builds.present?
render_show
else
redirect_to pipeline_path(@pipeline)
@@ -104,9 +97,18 @@ class Projects::PipelinesController < Projects::ApplicationController
@stage = pipeline.legacy_stage(params[:stage])
return not_found unless @stage
- respond_to do |format|
- format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } }
- end
+ render json: StageSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent(@stage, details: true)
+ end
+
+ # TODO: This endpoint is used by mini-pipeline-graph
+ # TODO: This endpoint should be migrated to `stage.json`
+ def stage_ajax
+ @stage = pipeline.legacy_stage(params[:stage])
+ return not_found unless @stage
+
+ render json: { html: view_to_html_string('projects/pipelines/_stage') }
end
def retry
@@ -157,7 +159,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create_params
- params.require(:pipeline).permit(:ref)
+ params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
end
def pipeline
@@ -172,4 +174,14 @@ class Projects::PipelinesController < Projects::ApplicationController
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339')
end
+
+ def authorize_update_pipeline!
+ return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
+ end
+
+ def limited_pipelines_count(project, scope = nil)
+ finder = PipelinesFinder.new(project, scope: scope)
+
+ view_context.limited_counter_with_delimiter(finder.execute)
+ end
end
diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb
index 1dd886409a5..c6b6243b553 100644
--- a/app/controllers/projects/prometheus/metrics_controller.rb
+++ b/app/controllers/projects/prometheus/metrics_controller.rb
@@ -25,7 +25,7 @@ module Projects
end
def require_prometheus_metrics!
- render_404 unless prometheus_adapter.can_query?
+ render_404 unless prometheus_adapter&.can_query?
end
end
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 937b0e39cbd..d01f324e6fd 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -28,11 +28,12 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def assign_archive_vars
- @id = params[:id]
-
- return unless @id
-
- @ref, @filename = extract_ref(@id)
+ if params[:id]
+ @ref, @filename = extract_ref(params[:id])
+ else
+ @ref = params[:ref]
+ @filename = nil
+ end
rescue InvalidPathError
render_404
end
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index 3cb01405b05..0ec2490655f 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -8,7 +8,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
return head(403) unless can?(current_user, :assign_runner, @runner)
- path = runners_path(project)
+ path = project_runners_path(project)
runner_project = @runner.assign_to(project, current_user)
if runner_project.persisted?
@@ -22,6 +22,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
- redirect_to runners_path(project), status: 302
+ redirect_to project_runners_path(project), status: 302
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index c950d0f7001..bef94cea989 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -1,6 +1,6 @@
class Projects::RunnersController < Projects::ApplicationController
before_action :authorize_admin_build!
- before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+ before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
layout 'project_settings'
@@ -13,7 +13,7 @@ class Projects::RunnersController < Projects::ApplicationController
def update
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
- redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ redirect_to project_runner_path(@project, @runner), notice: 'Runner was successfully updated.'
else
render 'edit'
end
@@ -24,26 +24,27 @@ class Projects::RunnersController < Projects::ApplicationController
@runner.destroy
end
- redirect_to runners_path(@project), status: 302
+ redirect_to project_runners_path(@project), status: 302
end
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
- redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
+ redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runners_path(@project), alert: 'Runner was not updated.'
+ redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
- redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
+ redirect_to project_runners_path(@project), notice: 'Runner was successfully updated.'
else
- redirect_to runners_path(@project), alert: 'Runner was not updated.'
+ redirect_to project_runners_path(@project), alert: 'Runner was not updated.'
end
end
def show
+ render 'shared/runners/show'
end
def toggle_shared_runners
@@ -52,9 +53,15 @@ class Projects::RunnersController < Projects::ApplicationController
redirect_to project_settings_ci_cd_path(@project)
end
+ def toggle_group_runners
+ project.toggle_ci_cd_settings!(:group_runners_enabled)
+
+ redirect_to project_settings_ci_cd_path(@project)
+ end
+
protected
- def set_runner
+ def runner
@runner ||= project.runners.find(params[:id])
end
diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb
index f7b70dd4b7b..7887bee49c5 100644
--- a/app/controllers/projects/settings/badges_controller.rb
+++ b/app/controllers/projects/settings/badges_controller.rb
@@ -1,12 +1,12 @@
module Projects
module Settings
class BadgesController < Projects::ApplicationController
- include GrapeRouteHelpers::NamedRouteMatcher
+ include API::Helpers::RelatedResourcesHelpers
before_action :authorize_admin_project!
def index
- @badge_api_endpoint = api_v4_projects_badges_path(id: @project.id)
+ @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id))
end
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index d80ef8113aa..1d850baf012 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -67,10 +67,18 @@ module Projects
def define_runners_variables
@project_runners = @project.runners.ordered
- @assignable_runners = current_user.ci_authorized_runners
- .assignable_for(project).ordered.page(params[:page]).per(20)
+
+ @assignable_runners = current_user
+ .ci_owned_runners
+ .assignable_for(project)
+ .ordered
+ .page(params[:page]).per(20)
+
@shared_runners = ::Ci::Runner.shared.active
+
@shared_runners_count = @shared_runners.count(:all)
+
+ @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
end
def define_secret_variables
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index 1ff08cce8cb..d9fecfecc40 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -11,7 +11,14 @@ module Projects
@hook = ProjectHook.new
# Services
- @services = @project.find_or_initialize_services
+ @services = @project.find_or_initialize_services(exceptions: service_exceptions)
+ end
+
+ private
+
+ # Returns a list of services that should be hidden from the list
+ def service_exceptions
+ @project.disabled_services.dup
end
end
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index f17056f13e0..4697af4f26a 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -2,6 +2,7 @@ module Projects
module Settings
class RepositoryController < Projects::ApplicationController
before_action :authorize_admin_project!
+ before_action :remote_mirror, only: [:show]
def show
render_show
@@ -25,6 +26,7 @@ module Projects
define_deploy_token
define_protected_refs
+ remote_mirror
render 'show'
end
@@ -41,6 +43,10 @@ module Projects
load_gon_index
end
+ def remote_mirror
+ @remote_mirror = project.remote_mirrors.first_or_initialize
+ end
+
def access_levels_options
{
create_access_levels: levels_for_dropdown,
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index c4930d3d18d..1b0751f48c5 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -29,8 +29,7 @@ class Projects::WikisController < Projects::ApplicationController
else
return render('empty') unless can?(current_user, :create_wiki, @project)
- @page = WikiPage.new(@project_wiki)
- @page.title = params[:id]
+ @page = build_page(title: params[:id])
render 'edit'
end
@@ -54,7 +53,7 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
- rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
+ rescue WikiPage::PageChangedError, WikiPage::PageRenameError, Gitlab::Git::Wiki::OperationError => e
@error = e
render 'edit'
end
@@ -70,6 +69,11 @@ class Projects::WikisController < Projects::ApplicationController
else
render action: "edit"
end
+ rescue Gitlab::Git::Wiki::OperationError => e
+ @page = build_page(wiki_params)
+ @error = e
+
+ render 'edit'
end
def history
@@ -94,6 +98,9 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to project_wiki_path(@project, :home),
status: 302,
notice: "Page was successfully deleted"
+ rescue Gitlab::Git::Wiki::OperationError => e
+ @error = e
+ render 'edit'
end
def git_access
@@ -116,4 +123,10 @@ class Projects::WikisController < Projects::ApplicationController
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
+
+ def build_page(args)
+ WikiPage.new(@project_wiki).tap do |page|
+ page.update_attributes(args)
+ end
+ end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 1848c806c41..f5a222b3a48 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,5 +1,6 @@
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
+ include AcceptsPendingInvitations
before_action :whitelist_query_limiting, only: [:destroy]
@@ -16,6 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
end
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
+ accept_pending_invitations
super
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
@@ -60,7 +62,7 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
- user.confirmed? ? dashboard_projects_path : users_almost_there_path
+ user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(resource)
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 04c36b3ebfe..93a71103a09 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -17,16 +17,20 @@ class SentNotificationsController < ApplicationController
flash[:notice] = "You have been unsubscribed from this thread."
if current_user
- case noteable
- when Issue
- redirect_to issue_path(noteable)
- when MergeRequest
- redirect_to merge_request_path(noteable)
- else
- redirect_to root_path
- end
+ redirect_to noteable_path(noteable)
else
redirect_to new_user_session_path
end
end
+
+ def noteable_path(noteable)
+ case noteable
+ when Issue
+ issue_path(noteable)
+ when MergeRequest
+ merge_request_path(noteable)
+ else
+ root_path
+ end
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index f3a4aa849c7..1a339f76d26 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,4 +1,5 @@
class SessionsController < Devise::SessionsController
+ include InternalRedirect
include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
@@ -102,18 +103,12 @@ class SessionsController < Devise::SessionsController
# we should never redirect to '/users/sign_in' after signing in successfully.
return true if redirect_uri.path == new_user_session_path
- redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri)
+ redirect_to = redirect_uri.to_s if host_allowed?(redirect_uri)
@redirect_to = redirect_to
store_location_for(:redirect, redirect_to)
end
- # Overridden in EE
- def redirect_allowed_to?(uri)
- uri.host == Gitlab.config.gitlab.host &&
- uri.port == Gitlab.config.gitlab.port
- end
-
def two_factor_enabled?
find_user&.two_factor_enabled?
end
diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb
new file mode 100644
index 00000000000..ab685b9106e
--- /dev/null
+++ b/app/controllers/users/terms_controller.rb
@@ -0,0 +1,70 @@
+module Users
+ class TermsController < ApplicationController
+ include InternalRedirect
+
+ skip_before_action :enforce_terms!
+ skip_before_action :check_password_expiration
+ skip_before_action :check_two_factor_requirement
+ skip_before_action :require_email
+
+ before_action :terms
+
+ layout 'terms'
+
+ def index
+ @redirect = redirect_path
+ end
+
+ def accept
+ agreement = Users::RespondToTermsService.new(current_user, viewed_term)
+ .execute(accepted: true)
+
+ if agreement.persisted?
+ redirect_to redirect_path
+ else
+ flash[:alert] = agreement.errors.full_messages.join(', ')
+ redirect_to terms_path, redirect: redirect_path
+ end
+ end
+
+ def decline
+ agreement = Users::RespondToTermsService.new(current_user, viewed_term)
+ .execute(accepted: false)
+
+ if agreement.persisted?
+ sign_out(current_user)
+ redirect_to root_path
+ else
+ flash[:alert] = agreement.errors.full_messages.join(', ')
+ redirect_to terms_path, redirect: redirect_path
+ end
+ end
+
+ private
+
+ def viewed_term
+ @viewed_term ||= ApplicationSetting::Term.find(params[:id])
+ end
+
+ def terms
+ unless @term = Gitlab::CurrentSettings.current_application_settings.latest_terms
+ redirect_to redirect_path
+ end
+ end
+
+ def redirect_path
+ redirect_to_path = safe_redirect_path(params[:redirect]) || safe_redirect_path_for_url(request.referer)
+
+ if redirect_to_path &&
+ excluded_redirect_paths.none? { |excluded| redirect_to_path.include?(excluded) }
+ redirect_to_path
+ else
+ root_path
+ end
+ end
+
+ def excluded_redirect_paths
+ [terms_path, new_user_session_path]
+ end
+ end
+end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index e72fd8eb3a5..051ea108e06 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -134,10 +134,8 @@ class GroupDescendantsFinder
end
def direct_child_projects
- GroupProjectsFinder.new(group: parent_group,
- current_user: current_user,
- options: { only_owned: true },
- params: params).execute
+ GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params)
+ .execute
end
# Finds all projects nested under `parent_group` or any of its descendant
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 0282b378d88..0754123a3cf 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -39,7 +39,7 @@ class GroupsFinder < UnionFinder
def all_groups
return [owned_groups] if params[:owned]
- return [Group.all] if current_user&.full_private_access?
+ return [Group.all] if current_user&.full_private_access? && all_available?
groups = []
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
@@ -67,6 +67,10 @@ class GroupsFinder < UnionFinder
end
def include_public_groups?
- current_user.nil? || params.fetch(:all_available, true)
+ current_user.nil? || all_available?
+ end
+
+ def all_available?
+ params.fetch(:all_available, true)
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 7ed9b1fc6d0..c6ef79ce15e 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -6,7 +6,7 @@
# klass - actual class like Issue or MergeRequest
# current_user - which user use
# params:
-# scope: 'created-by-me' or 'assigned-to-me' or 'all'
+# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'opened' or 'closed' or 'all'
# group_id: integer
# project_id: integer
@@ -282,9 +282,9 @@ class IssuableFinder
return items.none if current_user_related? && !current_user
case params[:scope]
- when 'created-by-me', 'authored'
+ when 'created_by_me', 'authored'
items.where(author_id: current_user.id)
- when 'assigned-to-me'
+ when 'assigned_to_me'
items.assigned_to(current_user)
else
items
@@ -426,6 +426,7 @@ class IssuableFinder
end
def current_user_related?
- params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
+ scope = params[:scope]
+ scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me'
end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 2a27ff0e386..1787b4899cd 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -5,7 +5,7 @@
# Arguments:
# current_user - which user use
# params:
-# scope: 'created-by-me' or 'assigned-to-me' or 'all'
+# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'open' or 'closed' or 'all'
# group_id: integer
# project_id: integer
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 64dc1e6af0f..e2240e5e0d8 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -5,7 +5,7 @@
# Arguments:
# current_user - which user use
# params:
-# scope: 'created-by-me' or 'assigned-to-me' or 'all'
+# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'open', 'closed', 'merged', or 'all'
# group_id: integer
# project_id: integer
diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb
index 3ad4bd5f066..5aea0cb8192 100644
--- a/app/finders/personal_projects_finder.rb
+++ b/app/finders/personal_projects_finder.rb
@@ -13,7 +13,7 @@ class PersonalProjectsFinder < UnionFinder
def execute(current_user = nil)
segments = all_projects(current_user)
- find_union(segments, Project).includes(:namespace).order_id_desc
+ find_union(segments, Project).includes(:namespace).order_updated_desc
end
private
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index f187a3b61fe..0a487839aff 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -14,6 +14,7 @@ class PipelinesFinder
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
+ items = by_sha(items)
items = by_name(items)
items = by_username(items)
items = by_yaml_errors(items)
@@ -69,6 +70,14 @@ class PipelinesFinder
end
end
+ def by_sha(items)
+ if params[:sha].present?
+ items.where(sha: params[:sha])
+ else
+ items
+ end
+ end
+
def by_name(items)
if params[:name].present?
items.joins(:user).where(users: { name: params[:name] })
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index edde8022ec9..65824a51919 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users)
users = by_external_identity(users)
users = by_external(users)
+ users = by_2fa(users)
users = by_created_at(users)
users = by_custom_attributes(users)
@@ -76,4 +77,15 @@ class UsersFinder
users.external
end
+
+ def by_2fa(users)
+ case params[:two_factor]
+ when 'enabled'
+ users.with_two_factor
+ when 'disabled'
+ users.without_two_factor
+ else
+ users
+ end
+ end
end
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
new file mode 100644
index 00000000000..97b6dac67c5
--- /dev/null
+++ b/app/helpers/active_sessions_helper.rb
@@ -0,0 +1,23 @@
+module ActiveSessionsHelper
+ # Maps a device type as defined in `ActiveSession` to an svg icon name and
+ # outputs the icon html.
+ #
+ # see `DeviceDetector::Device::DEVICE_NAMES` about the available device types
+ def active_session_device_type_icon(active_session)
+ icon_name =
+ case active_session.device_type
+ when 'smartphone', 'feature phone', 'phablet'
+ 'mobile'
+ when 'tablet'
+ 'tablet'
+ when 'tv', 'smart display', 'camera', 'portable media player', 'console'
+ 'media'
+ when 'car browser'
+ 'car'
+ else
+ 'monitor-o'
+ end
+
+ sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 228c8d2e8f9..f5d94ad96a1 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -2,6 +2,11 @@ require 'digest/md5'
require 'uri'
module ApplicationHelper
+ # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views
+ def render_if_exists(partial, locals = {})
+ render(partial, locals) if lookup_context.exists?(partial, [], true)
+ end
+
# Check if a particular controller is the current one
#
# args - One or more controller names to check
@@ -32,80 +37,6 @@ module ApplicationHelper
args.any? { |v| v.to_s.downcase == action_name }
end
- def project_icon(project_id, options = {})
- project =
- if project_id.respond_to?(:avatar_url)
- project_id
- else
- Project.find_by_full_path(project_id)
- end
-
- if project.avatar_url
- image_tag project.avatar_url, options
- else # generated icon
- project_identicon(project, options)
- end
- end
-
- def project_identicon(project, options = {})
- allowed_colors = {
- red: 'FFEBEE',
- purple: 'F3E5F5',
- indigo: 'E8EAF6',
- blue: 'E3F2FD',
- teal: 'E0F2F1',
- orange: 'FBE9E7',
- gray: 'EEEEEE'
- }
-
- options[:class] ||= ''
- options[:class] << ' identicon'
- bg_key = project.id % 7
- style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
-
- content_tag(:div, class: options[:class], style: style) do
- project.name[0, 1].upcase
- end
- end
-
- # Takes both user and email and returns the avatar_icon by
- # user (preferred) or email.
- def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
- if user
- avatar_icon_for_user(user, size, scale, only_path: only_path)
- elsif email
- avatar_icon_for_email(email, size, scale, only_path: only_path)
- else
- default_avatar
- end
- end
-
- def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
- user = User.find_by_any_email(email.try(:downcase))
- if user
- avatar_icon_for_user(user, size, scale, only_path: only_path)
- else
- gravatar_icon(email, size, scale)
- end
- end
-
- def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
- if user
- user.avatar_url(size: size, only_path: only_path) || default_avatar
- else
- gravatar_icon(nil, size, scale)
- end
- end
-
- def gravatar_icon(user_email = '', size = nil, scale = 2)
- GravatarService.new.execute(user_email, size, scale) ||
- default_avatar
- end
-
- def default_avatar
- asset_path('no_avatar.png')
- end
-
def last_commit(project)
if project.repo_exists?
time_ago_with_tooltip(project.repository.commit.committed_date)
@@ -332,4 +263,17 @@ module ApplicationHelper
_('You are on a read-only GitLab instance.')
end
+
+ def autocomplete_data_sources(object, noteable_type)
+ return {} unless object && noteable_type
+
+ {
+ members: members_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ issues: issues_project_autocomplete_sources_path(object),
+ merge_requests: merge_requests_project_autocomplete_sources_path(object),
+ labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
+ milestones: milestones_project_autocomplete_sources_path(object),
+ commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
+ }
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3fbb32c5229..b948e431882 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -248,7 +248,10 @@ module ApplicationSettingsHelper
:user_default_external,
:user_oauth_applications,
:version_check_enabled,
- :allow_local_requests_from_hooks_and_services
+ :allow_local_requests_from_hooks_and_services,
+ :enforce_terms,
+ :terms,
+ :mirror_available
]
end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index c109954f3a3..d2daee22aba 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,6 +1,6 @@
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
- FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
+ LDAP_PROVIDER = /\Aldap/
def ldap_enabled?
Gitlab::Auth::LDAP::Config.enabled?
@@ -23,7 +23,7 @@ module AuthHelper
end
def form_based_provider?(name)
- FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s }
+ [LDAP_PROVIDER, 'crowd'].any? { |pattern| pattern === name.to_s }
end
def form_based_providers
@@ -38,6 +38,10 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) }
end
+ def providers_for_base_controller
+ auth_providers.reject { |provider| LDAP_PROVIDER === provider }
+ end
+
def enabled_button_based_providers
disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || []
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 16451993e93..7b076728685 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -24,6 +24,15 @@ module AutoDevopsHelper
end
end
+ def cluster_ingress_ip(project)
+ project
+ .cluster_ingresses
+ .where("external_ip is not null")
+ .limit(1)
+ .pluck(:external_ip)
+ .first
+ end
+
private
def missing_auto_devops_domain?(project)
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 21b6c0a8ad5..43d92bde064 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -1,10 +1,84 @@
module AvatarsHelper
+ def project_icon(project_id, options = {})
+ project =
+ if project_id.respond_to?(:avatar_url)
+ project_id
+ else
+ Project.find_by_full_path(project_id)
+ end
+
+ if project.avatar_url
+ image_tag project.avatar_url, options
+ else # generated icon
+ project_identicon(project, options)
+ end
+ end
+
+ def project_identicon(project, options = {})
+ allowed_colors = {
+ red: 'FFEBEE',
+ purple: 'F3E5F5',
+ indigo: 'E8EAF6',
+ blue: 'E3F2FD',
+ teal: 'E0F2F1',
+ orange: 'FBE9E7',
+ gray: 'EEEEEE'
+ }
+
+ options[:class] ||= ''
+ options[:class] << ' identicon'
+ bg_key = project.id % 7
+ style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
+
+ content_tag(:div, class: options[:class], style: style) do
+ project.name[0, 1].upcase
+ end
+ end
+
+ # Takes both user and email and returns the avatar_icon by
+ # user (preferred) or email.
+ def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
+ if user
+ avatar_icon_for_user(user, size, scale, only_path: only_path)
+ elsif email
+ avatar_icon_for_email(email, size, scale, only_path: only_path)
+ else
+ default_avatar
+ end
+ end
+
+ def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
+ user = User.find_by_any_email(email.try(:downcase))
+ if user
+ avatar_icon_for_user(user, size, scale, only_path: only_path)
+ else
+ gravatar_icon(email, size, scale)
+ end
+ end
+
+ def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
+ if user
+ user.avatar_url(size: size, only_path: only_path) || default_avatar
+ else
+ gravatar_icon(nil, size, scale)
+ end
+ end
+
+ def gravatar_icon(user_email = '', size = nil, scale = 2)
+ GravatarService.new.execute(user_email, size, scale) ||
+ default_avatar
+ end
+
+ def default_avatar
+ ActionController::Base.helpers.image_path('no_avatar.png')
+ end
+
def author_avatar(commit_or_event, options = {})
user_avatar(options.merge({
user: commit_or_event.author,
user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email,
- css_class: 'hidden-xs'
+ css_class: 'd-none d-sm-inline'
}))
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index fef29789832..3db28fd6da3 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -17,7 +17,9 @@ module BlobHelper
end
def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
- "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
+ segments = [ide_path, 'project', project.full_path, 'edit', ref]
+ segments.concat(['-', path]) if path.present?
+ File.join(segments)
end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
@@ -331,7 +333,6 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
edit_disabled_button_tag(text, common_classes)
# This condition only applies to users who are logged in
- # Web IDE (Beta) requires the user to have this feature enabled
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
edit_link_tag(text, edit_path, common_classes)
elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index f0afcac5986..5fce97164ae 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -92,7 +92,7 @@ module CiStatusHelper
"pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
end
- def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
+ def render_project_pipeline_status(pipeline_status, tooltip_placement: 'left')
project = pipeline_status.project
path = pipelines_project_commit_path(project, pipeline_status.sha, ref: pipeline_status.ref)
@@ -103,7 +103,7 @@ module CiStatusHelper
tooltip_placement: tooltip_placement)
end
- def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
+ def render_commit_status(commit, ref: nil, tooltip_placement: 'left')
project = commit.project
path = pipelines_project_commit_path(project, commit, ref: ref)
@@ -114,7 +114,7 @@ module CiStatusHelper
tooltip_placement: tooltip_placement)
end
- def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
+ def render_pipeline_status(pipeline, tooltip_placement: 'left')
project = pipeline.project
path = project_pipeline_path(project, pipeline)
render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
@@ -125,7 +125,7 @@ module CiStatusHelper
Ci::Runner.shared.blank?
end
- def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body')
+ def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body')
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 7e4eb06b99d..c24d340d184 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -2,4 +2,12 @@ module ClustersHelper
def has_multiple_clusters?(project)
false
end
+
+ def render_gcp_signup_offer
+ return unless show_gcp_signup_offer?
+
+ content_tag :section, class: 'no-animate expanded' do
+ render 'projects/clusters/gcp_signup_offer_banner'
+ end
+ end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 98894b86551..e5c3be47801 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -27,7 +27,7 @@ module CommitsHelper
return unless @project && @ref
# Add the root project link and the arrow icon
- crumbs = content_tag(:li) do
+ crumbs = content_tag(:li, class: 'breadcrumb-item') do
link_to(
@project.path,
project_commits_path(@project, @ref)
@@ -38,7 +38,7 @@ module CommitsHelper
parts = @path.split('/')
parts.each_with_index do |part, i|
- crumbs << content_tag(:li) do
+ crumbs << content_tag(:li, class: 'breadcrumb-item') do
# The text is just the individual part, but the link needs all the parts before it
link_to(
part,
@@ -62,8 +62,8 @@ module CommitsHelper
# Returns a link formatted as a commit branch link
def commit_branch_link(url, text)
- link_to(url, class: 'label label-gray ref-name branch-link') do
- sprite_icon('fork', size: 16, css_class: 'fork-svg') + "#{text}"
+ link_to(url, class: 'badge badge-gray ref-name branch-link') do
+ sprite_icon('branch', size: 12, css_class: 'fork-svg') + "#{text}"
end
end
@@ -76,8 +76,8 @@ module CommitsHelper
# Returns a link formatted as a commit tag link
def commit_tag_link(url, text)
- link_to(url, class: 'label label-gray ref-name') do
- icon('tag', class: 'append-right-5') + "#{text}"
+ link_to(url, class: 'badge badge-gray ref-name') do
+ sprite_icon('tag', size: 12, css_class: 'append-right-5 vertical-align-middle') + "#{text}"
end
end
diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb
new file mode 100644
index 00000000000..5cd98f40f78
--- /dev/null
+++ b/app/helpers/count_helper.rb
@@ -0,0 +1,9 @@
+module CountHelper
+ def approximate_count_with_delimiters(count_data, model)
+ count = count_data[model]
+
+ raise "Missing model #{model} from count data" unless count
+
+ number_with_delimiter(count)
+ end
+end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 5089da519df..5a2360b4661 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -41,7 +41,7 @@ module DropdownsHelper
def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label]
- content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
+ content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down')
output.html_safe
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 079b3cd3aa0..cb6f709c604 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -41,7 +41,7 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: "event-filter-link has-tooltip",
+ class: "event-filter-link",
id: "#{key}_event_filter",
title: tooltip
}
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 7f3c118c7ab..61e12b0f31e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -19,14 +19,6 @@ module GitlabRoutingHelper
project_commits_path(project, ref_name, *args)
end
- def runners_path(project, *args)
- project_runners_path(project, *args)
- end
-
- def runner_path(runner, *args)
- project_runner_path(@project, runner, *args)
- end
-
def environment_path(environment, *args)
project_environment_path(environment.project, environment, *args)
end
@@ -81,6 +73,14 @@ module GitlabRoutingHelper
end
end
+ def edit_milestone_path(entity, *args)
+ if entity.parent.is_a?(Group)
+ edit_group_milestone_path(entity.parent, entity, *args)
+ else
+ edit_project_milestone_path(entity.parent, entity, *args)
+ end
+ end
+
def toggle_subscription_path(entity, *args)
if entity.is_a?(Issue)
toggle_subscription_project_issue_path(entity.project, entity)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 2f304b040c7..58372edff3c 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -60,7 +60,7 @@ module IconsHelper
def spinner(text = nil, visible = false)
css_class = 'loading'
- css_class << ' hide' unless visible
+ css_class << ' hidden' unless visible
content_tag :div, class: css_class do
icon('spinner spin') + text
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 06c3e569c84..8572c2b7276 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -9,6 +9,32 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
+ def sidebar_gutter_tooltip_text
+ sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
+ end
+
+ def sidebar_assignee_tooltip_label(issuable)
+ if issuable.assignee
+ issuable.assignee.name
+ else
+ issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
+ end
+ end
+
+ def sidebar_due_date_tooltip_label(issuable)
+ if issuable.due_date
+ "#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
+ else
+ _('Due date')
+ end
+ end
+
+ def due_date_remaining_days(issuable)
+ remaining_days_in_words = remaining_days_in_words(issuable)
+
+ "#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
+ end
+
def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any?
title = current_labels.first.try(:title)
@@ -131,15 +157,15 @@ module IssuablesHelper
output = ""
output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
- author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline-block", tooltip: true)
+ author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none")
end
output << "&ensp;".html_safe
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
- output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
- output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
+ output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block")
+ output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none d-lg-none d-xl-inline-block")
output.html_safe
end
@@ -153,10 +179,14 @@ module IssuablesHelper
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit }
- label_names = first.collect(&:name)
- label_names << "and #{last.size} more" unless last.empty?
+ if labels && labels.any?
+ label_names = first.collect(&:name)
+ label_names << "and #{last.size} more" unless last.empty?
- label_names.join(', ')
+ label_names.join(', ')
+ else
+ _("Labels")
+ end
end
def issuables_state_counter_text(issuable_type, state, display_count)
@@ -169,7 +199,7 @@ module IssuablesHelper
if display_count
count = issuables_count_for_state(issuable_type, state)
- html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
+ html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge badge-pill')
end
html.html_safe
@@ -321,7 +351,7 @@ module IssuablesHelper
def issuable_todo_button_data(issuable, todo, is_collapsed)
{
todo_text: "Add todo",
- mark_text: "Mark done",
+ mark_text: "Mark todo as done",
todo_icon: (is_collapsed ? icon('plus-square') : nil),
mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
issuable_id: issuable.id,
@@ -329,7 +359,8 @@ module IssuablesHelper
url: project_todos_path(@project),
delete_path: (dashboard_todo_path(todo) if todo),
placement: (is_collapsed ? 'left' : nil),
- container: (is_collapsed ? 'body' : nil)
+ container: (is_collapsed ? 'body' : nil),
+ boundary: 'viewport'
}
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index c4a6a1e4bb3..e1b0e7a4a3e 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -81,7 +81,7 @@ module LabelsHelper
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
- span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
+ span = %(<span class="badge color-label #{"has-tooltip" if tooltip}" ) +
%(style="background-color: #{label.color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index be8cb358de2..15a15405f1d 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -1,4 +1,6 @@
module MilestonesHelper
+ include EntityDateHelper
+
def milestones_filter_path(opts = {})
if @project
project_milestones_path(@project, opts)
@@ -72,9 +74,22 @@ module MilestonesHelper
end
end
+ def milestone_progress_tooltip_text(milestone)
+ has_issues = milestone.total_issues_count(current_user) > 0
+
+ if has_issues
+ [
+ _('Progress'),
+ _("%{percent}%% complete") % { percent: milestone.percent_complete(current_user) }
+ ].join('<br />')
+ else
+ _('Progress')
+ end
+ end
+
def milestone_progress_bar(milestone)
options = {
- class: 'progress-bar progress-bar-success',
+ class: 'progress-bar bg-success',
style: "width: #{milestone.percent_complete(current_user)}%;"
}
@@ -95,27 +110,69 @@ module MilestonesHelper
end
def milestone_tooltip_title(milestone)
- if milestone.due_date
- [milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
+ if milestone
+ "#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
end
end
- def milestone_remaining_days(milestone)
- if milestone.expired?
- content_tag(:strong, 'Past due')
- elsif milestone.upcoming?
- content_tag(:strong, 'Upcoming')
- elsif milestone.due_date
- time_ago = time_ago_in_words(milestone.due_date)
- content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
- content.slice!("about ")
- content << " remaining"
- content.html_safe
- elsif milestone.start_date && milestone.start_date.past?
- days = milestone.elapsed_days
- content = content_tag(:strong, days)
- content << " #{'day'.pluralize(days)} elapsed"
+ def milestone_time_for(date, date_type)
+ title = date_type == :start ? "Start date" : "End date"
+
+ if date
+ time_ago = time_ago_in_words(date)
+ time_ago.slice!("about ")
+
+ time_ago << if date.past?
+ " ago"
+ else
+ " remaining"
+ end
+
+ content = [
+ title,
+ "<br />",
+ date.to_s(:medium),
+ "(#{time_ago})"
+ ].join(" ")
+
content.html_safe
+ else
+ title
+ end
+ end
+
+ def milestone_issues_tooltip_text(milestone)
+ issues = milestone.count_issues_by_state(current_user)
+
+ return _("Issues") if issues.empty?
+
+ content = []
+
+ content << n_("1 open issue", "%d open issues", issues["opened"]) % issues["opened"] if issues["opened"]
+ content << n_("1 closed issue", "%d closed issues", issues["closed"]) % issues["closed"] if issues["closed"]
+
+ content.join('<br />').html_safe
+ end
+
+ def milestone_merge_requests_tooltip_text(milestone)
+ merge_requests = milestone.merge_requests
+
+ return _("Merge requests") if merge_requests.empty?
+
+ content = []
+
+ content << n_("1 open merge request", "%d open merge requests", merge_requests.opened.count) % merge_requests.opened.count if merge_requests.opened.any?
+ content << n_("1 closed merge request", "%d closed merge requests", merge_requests.closed.count) % merge_requests.closed.count if merge_requests.closed.any?
+ content << n_("1 merged merge request", "%d merged merge requests", merge_requests.merged.count) % merge_requests.merged.count if merge_requests.merged.any?
+
+ content.join('<br />').html_safe
+ end
+
+ def milestone_tooltip_due_date(milestone)
+ if milestone.due_date
+ "#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
+ else
+ _('Milestone')
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 56c88e6eab0..7754c34d6f0 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -28,7 +28,7 @@ module NavHelper
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
- elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access')
+ elsif current_controller?('wikis') && current_action?('show', 'create', 'edit', 'update', 'history', 'git_access', 'destroy')
%w[page-gutter wiki-sidebar right-sidebar-expanded]
else
[]
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index a64b2acdd77..fa54eafd3a3 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -257,6 +257,7 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
+ nav_tabs << :operations
end
if project.external_issue_tracker
@@ -400,7 +401,8 @@ module ProjectsHelper
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
- filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
+ disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+ filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]")
end
def project_child_container_class(view_path)
@@ -441,7 +443,7 @@ module ProjectsHelper
visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'),
- lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
+ lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
}
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 00fe67d6ffb..5b4a141dbcf 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -1,14 +1,14 @@
module SystemNoteHelper
ICON_NAMES_BY_ACTION = {
'commit' => 'commit',
- 'description' => 'pencil',
+ 'description' => 'pencil-square',
'merge' => 'git-merge',
'merged' => 'git-merge',
'opened' => 'issue-open',
'closed' => 'issue-close',
'time_tracking' => 'timer',
'assignee' => 'user',
- 'title' => 'pencil',
+ 'title' => 'pencil-square',
'task' => 'task-done',
'label' => 'label',
'cross_reference' => 'comment-dots',
@@ -18,7 +18,7 @@ module SystemNoteHelper
'milestone' => 'clock',
'discussion' => 'comment',
'moved' => 'arrow-right',
- 'outdated' => 'pencil',
+ 'outdated' => 'pencil-square',
'duplicate' => 'issue-duplicate',
'locked' => 'lock',
'unlocked' => 'lock-open'
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 36abfaf19a5..da5fe25c07d 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -1,11 +1,16 @@
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
+ GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
!user_dismissed?(GKE_CLUSTER_INTEGRATION)
end
+ def show_gcp_signup_offer?
+ !user_dismissed?(GCP_SIGNUP_OFFER)
+ end
+
private
def user_dismissed?(feature_name)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 01af68088df..ce9373f5883 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -23,9 +23,31 @@ module UsersHelper
profile_tabs.include?(tab)
end
+ def current_user_menu_items
+ @current_user_menu_items ||= get_current_user_menu_items
+ end
+
+ def current_user_menu?(item)
+ current_user_menu_items.include?(item)
+ end
+
private
def get_profile_tabs
[:activity, :groups, :contributed, :projects, :snippets]
end
+
+ def get_current_user_menu_items
+ items = []
+
+ items << :sign_out if current_user
+
+ return items if current_user&.required_terms_not_accepted?
+
+ items << :help
+ items << :profile if can?(current_user, :read_user, current_user)
+ items << :settings if can?(current_user, :update_user, current_user)
+
+ items
+ end
end
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
index 8bcced70d63..e12e4ba70e9 100644
--- a/app/helpers/webpack_helper.rb
+++ b/app/helpers/webpack_helper.rb
@@ -1,12 +1,12 @@
-require 'webpack/rails/manifest'
+require 'gitlab/webpack/manifest'
module WebpackHelper
- def webpack_bundle_tag(bundle, force_same_domain: false)
- javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain))
+ def webpack_bundle_tag(bundle)
+ javascript_include_tag(*webpack_entrypoint_paths(bundle))
end
def webpack_controller_bundle_tags
- bundles = []
+ chunks = []
action = case controller.action_name
when 'create' then 'new'
@@ -16,37 +16,44 @@ module WebpackHelper
route = [*controller.controller_path.split('/'), action].compact
- until route.empty?
+ until chunks.any? || route.empty?
+ entrypoint = "pages.#{route.join('.')}"
begin
- asset_paths = gitlab_webpack_asset_paths("pages.#{route.join('.')}", extension: 'js')
- bundles.unshift(*asset_paths)
- rescue Webpack::Rails::Manifest::EntryPointMissingError
+ chunks = webpack_entrypoint_paths(entrypoint, extension: 'js')
+ rescue Gitlab::Webpack::Manifest::AssetMissingError
# no bundle exists for this path
end
-
route.pop
end
- javascript_include_tag(*bundles)
+ if chunks.empty?
+ chunks = webpack_entrypoint_paths("default", extension: 'js')
+ end
+
+ javascript_include_tag(*chunks)
end
- # override webpack-rails gem helper until changes can make it upstream
- def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
+ def webpack_entrypoint_paths(source, extension: nil, exclude_duplicates: true)
return "" unless source.present?
- paths = Webpack::Rails::Manifest.asset_paths(source)
+ paths = Gitlab::Webpack::Manifest.entrypoint_paths(source)
if extension
paths.select! { |p| p.ends_with? ".#{extension}" }
end
- unless force_same_domain
- force_host = webpack_public_host
- if force_host
- paths.map! { |p| "#{force_host}#{p}" }
- end
+ force_host = webpack_public_host
+ if force_host
+ paths.map! { |p| "#{force_host}#{p}" }
end
- paths
+ if exclude_duplicates
+ @used_paths ||= []
+ new_paths = paths - @used_paths
+ @used_paths += new_paths
+ new_paths
+ else
+ paths
+ end
end
def webpack_public_host
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index b3f2aeb08ca..5ba3a4a322c 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -56,6 +56,14 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
+ def merge_request_unmergeable_email(recipient_id, merge_request_id, reason = nil)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @reasons = MergeRequestPresenter.new(@merge_request, current_user: current_user).unmergeable_reasons
+
+ mail_answer_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason))
+ end
+
def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 50e17fe7717..d9a6fe2a41e 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -43,7 +43,7 @@ module Emails
private
def note_target_url_options
- [@project, @note.noteable, anchor: "note_#{@note.id}"]
+ [@project || @group, @note.noteable, anchor: "note_#{@note.id}"]
end
def note_thread_options(recipient_id)
@@ -58,8 +58,9 @@ module Emails
# `note_id` is a `Note` when originating in `NotifyPreview`
@note = note_id.is_a?(Note) ? note_id : Note.find(note_id)
@project = @note.project
+ @group = @note.noteable.try(:group)
- if @project && @note.persisted?
+ if (@project || @group) && @note.persisted?
@sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index e4212775956..1db1482d6b7 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -16,6 +16,7 @@ class Notify < BaseMailer
helper BlobHelper
helper EmailsHelper
helper MembersHelper
+ helper AvatarsHelper
helper GitlabRoutingHelper
def test_email(recipient_email, subject, body)
@@ -93,6 +94,7 @@ class Notify < BaseMailer
def subject(*extra)
subject = ""
subject << "#{@project.name} | " if @project
+ subject << "#{@group.name} | " if @group
subject << extra.join(' | ') if extra.present?
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
@@ -116,10 +118,9 @@ class Notify < BaseMailer
@reason = headers['X-GitLab-NotificationReason']
if Gitlab::IncomingEmail.enabled? && @sent_notification
- address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
- address.display_name = @project.full_name
-
- headers['Reply-To'] = address
+ headers['Reply-To'] = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)).tap do |address|
+ address.display_name = reply_display_name(model)
+ end
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= []
@@ -131,6 +132,11 @@ class Notify < BaseMailer
mail(headers)
end
+ # `model` is used on EE code
+ def reply_display_name(_model)
+ @project.full_name
+ end
+
# Send an email that starts a new conversation thread,
# with headers suitable for grouping by thread in email clients.
#
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 618d4af4272..bb600eaccba 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -10,6 +10,14 @@ class Ability
end
end
+ # Given a list of users and a group this method returns the users that can
+ # read the given group.
+ def users_that_can_read_group(users, group)
+ DeclarativePolicy.subject_scope do
+ users.select { |u| allowed?(u, :read_group, group) }
+ end
+ end
+
# Given a list of users and a snippet this method returns the users that can
# read the given snippet.
def users_that_can_read_personal_snippet(users, snippet)
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
new file mode 100644
index 00000000000..b4a86dbb331
--- /dev/null
+++ b/app/models/active_session.rb
@@ -0,0 +1,110 @@
+class ActiveSession
+ include ActiveModel::Model
+
+ attr_accessor :created_at, :updated_at,
+ :session_id, :ip_address,
+ :browser, :os, :device_name, :device_type
+
+ def current?(session)
+ return false if session_id.nil? || session.id.nil?
+
+ session_id == session.id
+ end
+
+ def human_device_type
+ device_type&.titleize
+ end
+
+ def self.set(user, request)
+ Gitlab::Redis::SharedState.with do |redis|
+ session_id = request.session.id
+ client = DeviceDetector.new(request.user_agent)
+ timestamp = Time.current
+
+ active_user_session = new(
+ ip_address: request.ip,
+ browser: client.name,
+ os: client.os_name,
+ device_name: client.device_name,
+ device_type: client.device_type,
+ created_at: user.current_sign_in_at || timestamp,
+ updated_at: timestamp,
+ session_id: session_id
+ )
+
+ redis.pipelined do
+ redis.setex(
+ key_name(user.id, session_id),
+ Settings.gitlab['session_expire_delay'] * 60,
+ Marshal.dump(active_user_session)
+ )
+
+ redis.sadd(
+ lookup_key_name(user.id),
+ session_id
+ )
+ end
+ end
+ end
+
+ def self.list(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ cleaned_up_lookup_entries(redis, user.id).map do |entry|
+ # rubocop:disable Security/MarshalLoad
+ Marshal.load(entry)
+ # rubocop:enable Security/MarshalLoad
+ end
+ end
+ end
+
+ def self.destroy(user, session_id)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.srem(lookup_key_name(user.id), session_id)
+
+ deleted_keys = redis.del(key_name(user.id, session_id))
+
+ # only allow deleting the devise session if we could actually find a
+ # related active session. this prevents another user from deleting
+ # someone else's session.
+ if deleted_keys > 0
+ redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
+ end
+ end
+ end
+
+ def self.cleanup(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ cleaned_up_lookup_entries(redis, user.id)
+ end
+ end
+
+ def self.key_name(user_id, session_id = '*')
+ "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
+ end
+
+ def self.lookup_key_name(user_id)
+ "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
+ end
+
+ def self.cleaned_up_lookup_entries(redis, user_id)
+ lookup_key = lookup_key_name(user_id)
+
+ session_ids = redis.smembers(lookup_key)
+
+ entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+ return [] if entry_keys.empty?
+
+ entries = redis.mget(entry_keys)
+
+ session_ids_and_entries = session_ids.zip(entries)
+
+ # remove expired keys.
+ # only the single key entries are automatically expired by redis, the
+ # lookup entries in the set need to be removed manually.
+ session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
+ redis.srem(lookup_key, session_id)
+ end
+
+ session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
+ end
+end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index fb66dd0b766..67cc84a9140 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,7 +1,8 @@
class Appearance < ActiveRecord::Base
+ include CacheableAttributes
include CacheMarkdownField
- include AfterCommitQueue
include ObjectStorage::BackgroundMove
+ include WithUploads
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
@@ -14,18 +15,9 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
- CACHE_KEY = "current_appearance:#{Gitlab::VERSION}".freeze
-
- after_commit :flush_redis_cache
-
- def self.current
- Rails.cache.fetch(CACHE_KEY) { first }
- end
-
- def flush_redis_cache
- Rails.cache.delete(CACHE_KEY)
+ # Overrides CacheableAttributes.current_without_cache
+ def self.current_without_cache
+ first
end
def single_appearance_row
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 862933bf127..e8ccb320fae 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -1,11 +1,11 @@
class ApplicationSetting < ActiveRecord::Base
+ include CacheableAttributes
include CacheMarkdownField
include TokenAuthenticatable
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
- CACHE_KEY = 'application_setting.last'.freeze
DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
| # or
\s # any whitespace character
@@ -220,46 +220,15 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ validate :terms_exist, if: :enforce_terms?
+
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
after_commit do
- Rails.cache.write(CACHE_KEY, self)
- end
-
- def self.current
- ensure_cache_setup
-
- Rails.cache.fetch(CACHE_KEY) do
- ApplicationSetting.last.tap do |settings|
- # do not cache nils
- raise 'missing settings' unless settings
- end
- end
- rescue
- # Fall back to an uncached value if there are any problems (e.g. redis down)
- ApplicationSetting.last
- end
-
- def self.expire
- Rails.cache.delete(CACHE_KEY)
- rescue
- # Gracefully handle when Redis is not available. For example,
- # omnibus may fail here during gitlab:assets:compile.
- end
-
- def self.cached
- value = Rails.cache.read(CACHE_KEY)
- ensure_cache_setup if value.present?
- value
- end
-
- def self.ensure_cache_setup
- # This is a workaround for a Rails bug that causes attribute methods not
- # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
- ApplicationSetting.define_attribute_methods
+ reset_memoized_terms
end
def self.defaults
@@ -331,7 +300,8 @@ class ApplicationSetting < ActiveRecord::Base
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitaly_timeout_default: 55,
- allow_local_requests_from_hooks_and_services: false
+ allow_local_requests_from_hooks_and_services: false,
+ mirror_available: true
}
end
@@ -507,6 +477,16 @@ class ApplicationSetting < ActiveRecord::Base
password_authentication_enabled_for_web? || password_authentication_enabled_for_git?
end
+ delegate :terms, to: :latest_terms, allow_nil: true
+ def latest_terms
+ @latest_terms ||= Term.latest
+ end
+
+ def reset_memoized_terms
+ @latest_terms = nil
+ latest_terms
+ end
+
private
def ensure_uuid!
@@ -520,4 +500,10 @@ class ApplicationSetting < ActiveRecord::Base
errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
invalid.empty?
end
+
+ def terms_exist
+ return unless enforce_terms?
+
+ errors.add(:terms, "You need to set terms to be enforced") unless terms.present?
+ end
end
diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb
new file mode 100644
index 00000000000..e8ce0ccbb71
--- /dev/null
+++ b/app/models/application_setting/term.rb
@@ -0,0 +1,13 @@
+class ApplicationSetting
+ class Term < ActiveRecord::Base
+ include CacheMarkdownField
+
+ validates :terms, presence: true
+
+ cache_markdown_field :terms
+
+ def self.latest
+ order(:id).last
+ end
+ end
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1c8d9ca4aa5..75fd55a8f7b 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -19,14 +19,16 @@ module Ci
has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
+ has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
- has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+ delegate :gitlab_deploy_token, to: :project
##
# The "environment" field for builds is a String, and is the unexpanded name!
@@ -95,8 +97,8 @@ module Ci
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
- after_commit :update_project_statistics_after_save, on: [:create, :update]
- after_commit :update_project_statistics, on: :destroy
+ after_save :update_project_statistics_after_save, if: :artifacts_size_changed?
+ after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
class << self
# This is needed for url_for to work,
@@ -182,7 +184,7 @@ module Ci
end
def playable?
- action? && (manual? || complete?)
+ action? && (manual? || retryable?)
end
def action?
@@ -597,6 +599,7 @@ module Ci
break variables unless persisted?
variables
+ .concat(pipeline.persisted_variables)
.append(key: 'CI_JOB_ID', value: id.to_s)
.append(key: 'CI_JOB_TOKEN', value: token, public: false)
.append(key: 'CI_BUILD_ID', value: id.to_s)
@@ -604,6 +607,7 @@ module Ci
.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)
+ .concat(deploy_token_variables)
end
end
@@ -611,10 +615,10 @@ module Ci
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.namespace.features.join(','))
+ 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_REVISION', value: Gitlab::REVISION)
+ 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)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
@@ -654,6 +658,15 @@ module Ci
end
end
+ def deploy_token_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless gitlab_deploy_token
+
+ variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.username)
+ variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
+ end
+ end
+
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
@@ -664,16 +677,20 @@ module Ci
pipeline.config_processor.build_attributes(name)
end
- def update_project_statistics
- return unless project
+ def update_project_statistics_after_save
+ update_project_statistics(read_attribute(:artifacts_size).to_i - artifacts_size_was.to_i)
+ end
- ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
+ def update_project_statistics_after_destroy
+ update_project_statistics(-artifacts_size)
end
- def update_project_statistics_after_save
- if previous_changes.include?('artifacts_size')
- update_project_statistics
- end
+ def update_project_statistics(difference)
+ ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
+ end
+
+ def project_destroyed?
+ project.pending_delete?
end
end
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
new file mode 100644
index 00000000000..4856f10846c
--- /dev/null
+++ b/app/models/ci/build_trace_chunk.rb
@@ -0,0 +1,180 @@
+module Ci
+ class BuildTraceChunk < ActiveRecord::Base
+ include FastDestroyAll
+ extend Gitlab::Ci::Model
+
+ belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+
+ default_value_for :data_store, :redis
+
+ WriteError = Class.new(StandardError)
+
+ CHUNK_SIZE = 128.kilobytes
+ CHUNK_REDIS_TTL = 1.week
+ WRITE_LOCK_RETRY = 10
+ WRITE_LOCK_SLEEP = 0.01.seconds
+ WRITE_LOCK_TTL = 1.minute
+
+ enum data_store: {
+ redis: 1,
+ db: 2
+ }
+
+ class << self
+ def redis_data_key(build_id, chunk_index)
+ "gitlab:ci:trace:#{build_id}:chunks:#{chunk_index}"
+ end
+
+ def redis_data_keys
+ redis.pluck(:build_id, :chunk_index).map do |data|
+ redis_data_key(data.first, data.second)
+ end
+ end
+
+ def redis_delete_data(keys)
+ return if keys.empty?
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(keys)
+ end
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def begin_fast_destroy
+ redis_data_keys
+ end
+
+ ##
+ # FastDestroyAll concerns
+ def finalize_fast_destroy(keys)
+ redis_delete_data(keys)
+ end
+ end
+
+ ##
+ # Data is memoized for optimizing #size and #end_offset
+ def data
+ @data ||= get_data.to_s
+ end
+
+ def truncate(offset = 0)
+ raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
+ return if offset == size # Skip the following process as it doesn't affect anything
+
+ self.append("", offset)
+ end
+
+ def append(new_data, offset)
+ raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
+ raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
+
+ set_data(data.byteslice(0, offset) + new_data)
+ end
+
+ def size
+ data&.bytesize.to_i
+ end
+
+ def start_offset
+ chunk_index * CHUNK_SIZE
+ end
+
+ def end_offset
+ start_offset + size
+ end
+
+ def range
+ (start_offset...end_offset)
+ end
+
+ def use_database!
+ in_lock do
+ break if db?
+ break unless size > 0
+
+ self.update!(raw_data: data, data_store: :db)
+ self.class.redis_delete_data([redis_data_key])
+ end
+ end
+
+ private
+
+ def get_data
+ if redis?
+ redis_data
+ elsif db?
+ raw_data
+ else
+ raise 'Unsupported data store'
+ end&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
+ end
+
+ def set_data(value)
+ raise ArgumentError, 'too much data' if value.bytesize > CHUNK_SIZE
+
+ in_lock do
+ if redis?
+ redis_set_data(value)
+ elsif db?
+ self.raw_data = value
+ else
+ raise 'Unsupported data store'
+ end
+
+ @data = value
+
+ save! if changed?
+ end
+
+ schedule_to_db if full?
+ end
+
+ def schedule_to_db
+ return if db?
+
+ Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ end
+
+ def full?
+ size == CHUNK_SIZE
+ end
+
+ def redis_data
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(redis_data_key)
+ end
+ end
+
+ def redis_set_data(data)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_data_key, data, ex: CHUNK_REDIS_TTL)
+ end
+ end
+
+ def redis_data_key
+ self.class.redis_data_key(build_id, chunk_index)
+ end
+
+ def in_lock
+ write_lock_key = "trace_write:#{build_id}:chunks:#{chunk_index}"
+
+ lease = Gitlab::ExclusiveLease.new(write_lock_key, timeout: WRITE_LOCK_TTL)
+ retry_count = 0
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit between retries.
+ sleep(WRITE_LOCK_SLEEP)
+ break if WRITE_LOCK_RETRY < (retry_count += 1)
+ end
+
+ raise WriteError, 'Failed to obtain write lock' unless uuid
+
+ self.reload if self.persisted?
+ return yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(write_lock_key, uuid)
+ end
+ end
+end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index fbb95fe16df..3b952391b7e 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -7,12 +7,15 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
- before_save :update_file_store
+ mount_uploader :file, JobArtifactUploader
+
before_save :set_size, if: :file_changed?
+ after_save :update_project_statistics_after_save, if: :size_changed?
+ after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
- scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
+ after_save :update_file_store, if: :file_changed?
- mount_uploader :file, JobArtifactUploader
+ scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
delegate :exists?, :open, to: :file
@@ -23,7 +26,9 @@ module Ci
}
def update_file_store
- self.file_store = file.object_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
end
def self.artifacts_size_for(project)
@@ -34,10 +39,6 @@ module Ci
[nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store)
end
- def set_size
- self.size = file.size
- end
-
def expire_in
expire_at - Time.now if expire_at
end
@@ -48,5 +49,28 @@ module Ci
ChronicDuration.parse(value)&.seconds&.from_now
end
end
+
+ private
+
+ def set_size
+ self.size = file.size
+ end
+
+ def update_project_statistics_after_save
+ update_project_statistics(size.to_i - size_was.to_i)
+ end
+
+ def update_project_statistics_after_destroy
+ update_project_statistics(-self.size)
+ end
+
+ def update_project_statistics(difference)
+ ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
+ end
+
+ def project_destroyed?
+ # Use job.project to avoid extra DB query for project
+ job.project.pending_delete?
+ end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 434b9b64c65..53af87a271a 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -32,15 +32,21 @@ module Ci
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
+ accepts_nested_attributes_for :variables, reject_if: :persisted?
+
delegate :id, to: :project, prefix: true
delegate :full_path, to: :project, prefix: true
- validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
+ # Replace validator below with
+ # `validates :source, presence: { unless: :importing? }, on: :create`
+ # when removing Gitlab.rails5? code.
+ validate :valid_source, unless: :importing?, on: :create
+
after_create :keep_around_commits, unless: :importing?
enum source: {
@@ -269,19 +275,39 @@ module Ci
end
def git_author_name
- commit.try(:author_name)
+ strong_memoize(:git_author_name) do
+ commit.try(:author_name)
+ end
end
def git_author_email
- commit.try(:author_email)
+ strong_memoize(:git_author_email) do
+ commit.try(:author_email)
+ end
end
def git_commit_message
- commit.try(:message)
+ strong_memoize(:git_commit_message) do
+ commit.try(:message)
+ end
end
def git_commit_title
- commit.try(:title)
+ strong_memoize(:git_commit_title) do
+ commit.try(:title)
+ end
+ end
+
+ def git_commit_full_title
+ strong_memoize(:git_commit_full_title) do
+ commit.try(:full_title)
+ end
+ end
+
+ def git_commit_description
+ strong_memoize(:git_commit_description) do
+ commit.try(:description)
+ end
end
def short_sha
@@ -380,7 +406,18 @@ module Ci
end
def has_warnings?
- builds.latest.failed_but_allowed.any?
+ number_of_warnings.positive?
+ end
+
+ def number_of_warnings
+ BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
+ Build.where(commit_id: pipeline_ids)
+ .latest
+ .failed_but_allowed
+ .group(:commit_id)
+ .count
+ .each { |id, amount| loader.call(id, amount) }
+ end
end
def set_config_source
@@ -486,11 +523,19 @@ module Ci
strong_memoize(:legacy_trigger) { trigger_requests.first }
end
+ def persisted_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) if persisted?
+ end
+ end
+
def predefined_variables
Gitlab::Ci::Variables::Collection.new
- .append(key: 'CI_PIPELINE_ID', value: id.to_s)
.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
+ .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message)
+ .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title)
+ .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description)
end
def queued_duration
@@ -530,6 +575,17 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end
+ # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
+ # They always return `false`.
+ # These methods overwrite autogenerated ones to return correct results.
+ def unknown?
+ Gitlab.rails5? ? source.nil? : super
+ end
+
+ def unknown_source?
+ Gitlab.rails5? ? config_source.nil? : super
+ end
+
private
def ci_yaml_from_repo
@@ -565,5 +621,11 @@ module Ci
project.repository.keep_around(self.sha)
project.repository.keep_around(self.before_sha)
end
+
+ def valid_source
+ if source.nil? || source == "unknown"
+ errors.add(:source, "invalid source")
+ end
+ end
end
end
diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb
index de5aae17a15..38e14ffbc0c 100644
--- a/app/models/ci/pipeline_variable.rb
+++ b/app/models/ci/pipeline_variable.rb
@@ -5,6 +5,8 @@ module Ci
belongs_to :pipeline
+ alias_attribute :secret_value, :value
+
validates :key, uniqueness: { scope: :pipeline_id }
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ee0d8df8eb7..530eacf4be0 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -13,33 +13,52 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :runner_projects
+ has_many :projects, through: :runner_projects
+ has_many :runner_namespaces
+ has_many :groups, through: :runner_namespaces
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
before_validation :set_default_values
- scope :specific, ->() { where(is_shared: false) }
- scope :shared, ->() { where(is_shared: true) }
- scope :active, ->() { where(active: true) }
- scope :paused, ->() { where(active: false) }
- scope :online, ->() { where('contacted_at > ?', contact_time_deadline) }
- scope :ordered, ->() { order(id: :desc) }
+ scope :specific, -> { where(is_shared: false) }
+ scope :shared, -> { where(is_shared: true) }
+ scope :active, -> { where(active: true) }
+ scope :paused, -> { where(active: false) }
+ scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
+ scope :ordered, -> { order(id: :desc) }
- scope :owned_or_shared, ->(project_id) do
- joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
- .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ scope :belonging_to_project, -> (project_id) {
+ joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
+ }
+
+ scope :belonging_to_parent_group_of_project, -> (project_id) {
+ project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
+ hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups).base_and_ancestors
+
+ joins(:groups).where(namespaces: { id: hierarchy_groups })
+ }
+
+ scope :owned_or_shared, -> (project_id) do
+ union = Gitlab::SQL::Union.new(
+ [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
+ remove_duplicates: false
+ )
+ from("(#{union.to_sql}) ci_runners")
end
scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match.
where(locked: false)
- .where.not("id IN (#{project.runners.select(:id).to_sql})").specific
+ .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})")
+ .project_type
end
validate :tag_constraints
+ validate :either_projects_or_group
validates :access_level, presence: true
+ validates :runner_type, presence: true
acts_as_taggable
@@ -50,7 +69,13 @@ module Ci
ref_protected: 1
}
- cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
+ enum runner_type: {
+ instance_type: 1,
+ group_type: 2,
+ project_type: 3
+ }
+
+ cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
@@ -83,7 +108,13 @@ module Ci
end
def assign_to(project, current_user = nil)
- self.is_shared = false if shared?
+ if shared?
+ self.is_shared = false if shared?
+ self.runner_type = :project_type
+ elsif group_type?
+ raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
+ end
+
self.save
project.runner_projects.create(runner_id: self.id)
end
@@ -120,6 +151,14 @@ module Ci
!shared?
end
+ def assigned_to_group?
+ runner_namespaces.any?
+ end
+
+ def assigned_to_project?
+ runner_projects.any?
+ end
+
def can_pick?(build)
return false if self.ref_protected? && !build.protected?
@@ -174,6 +213,12 @@ module Ci
end
end
+ def pick_build!(build)
+ if can_pick?(build)
+ tick_runner_queue
+ end
+ end
+
private
def cleanup_runner_queue
@@ -205,7 +250,17 @@ module Ci
end
def assignable_for?(project_id)
- is_shared? || projects.exists?(id: project_id)
+ self.class.owned_or_shared(project_id).where(id: self.id).any?
+ end
+
+ def either_projects_or_group
+ if groups.many?
+ errors.add(:runner, 'can only be assigned to one group')
+ end
+
+ if assigned_to_group? && assigned_to_project?
+ errors.add(:runner, 'can only be assigned either to projects or to a group')
+ end
end
def accepting_tags?(build)
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
new file mode 100644
index 00000000000..3269f86e8ca
--- /dev/null
+++ b/app/models/ci/runner_namespace.rb
@@ -0,0 +1,9 @@
+module Ci
+ class RunnerNamespace < ActiveRecord::Base
+ extend Gitlab::Ci::Model
+
+ belongs_to :runner
+ belongs_to :namespace, class_name: '::Namespace'
+ belongs_to :group, class_name: '::Group', foreign_key: :namespace_id
+ end
+end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 75b8ea2a371..5a1eeb966aa 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -13,14 +13,27 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
- validates :project, presence: true, unless: :importing?
- validates :pipeline, presence: true, unless: :importing?
- validates :name, presence: true, unless: :importing?
+ with_options unless: :importing? do
+ validates :project, presence: true
+ validates :pipeline, presence: true
+ validates :name, presence: true
+ validates :position, presence: true
+ end
- after_initialize do |stage|
+ after_initialize do
self.status = DEFAULT_STATUS if self.status.nil?
end
+ before_validation unless: :importing? do
+ next if position.present?
+
+ self.position = statuses.select(:stage_idx)
+ .where('stage_idx IS NOT NULL')
+ .group(:stage_idx)
+ .order('COUNT(*) DESC')
+ .first&.stage_idx.to_i
+ end
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 7b25d8c4089..c702c4ee807 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -49,6 +49,11 @@ module Clusters
# ensures headers containing auth data are appended to original k8s client options
options = kube_client.rest_client.options.merge(headers: kube_client.headers)
RestClient::Resource.new(proxy_url, options)
+ rescue Kubeclient::HttpError
+ # If users have mistakenly set parameters or removed the depended clusters,
+ # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
+ # Since `PrometheusAdapter#can_query?` is eargely loaded on environement pages in gitlab,
+ # we need to silence the exceptions
end
private
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 16efe90fa27..b881b4eaf36 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -43,12 +43,20 @@ module Clusters
def create_and_assign_runner
transaction do
- project.runners.create!(name: 'kubernetes-cluster', tag_list: %w(kubernetes cluster)).tap do |runner|
+ project.runners.create!(runner_create_params).tap do |runner|
update!(runner_id: runner.id)
end
end
end
+ def runner_create_params
+ {
+ name: 'kubernetes-cluster',
+ runner_type: :project_type,
+ tag_list: %w(kubernetes cluster)
+ }
+ end
+
def gitlab_url
Gitlab::Routing.url_helpers.root_url(only_path: false)
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index e4a06f3f976..77947d515c1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -15,7 +15,7 @@ module Clusters
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
- has_many :projects, -> { auto_include(false) }, through: :cluster_projects, class_name: '::Project'
+ has_many :projects, through: :cluster_projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
diff --git a/app/models/commit.rb b/app/models/commit.rb
index de860df4b9c..56d4c86774e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -105,6 +105,10 @@ class Commit
end
end
end
+
+ def parent_class
+ ::Project
+ end
end
attr_accessor :raw
@@ -220,8 +224,34 @@ class Commit
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
+ 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) }
+ end
+ end
+
def author
- User.find_by_any_email(author_email.downcase)
+ # We use __sync so that we get the actual objects back (including an actual
+ # nil), instead of a wrapper, as returning a wrapped nil breaks a lot of
+ # code.
+ lazy_author.__sync
end
request_cache(:author) { author_email.downcase }
@@ -248,7 +278,7 @@ class Commit
end
def notes_with_associations
- notes.includes(:author)
+ notes.includes(:author, :award_emoji)
end
def merge_requests
@@ -420,6 +450,12 @@ class Commit
# no-op but needs to be defined since #persisted? is defined
end
+ def touch_later
+ # No-op.
+ # This method is called by ActiveRecord.
+ # We don't want to do anything for `Commit` model, so this is empty.
+ end
+
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
def work_in_progress?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index b6276c2fb50..a7d05722287 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -2,6 +2,7 @@ class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
include AfterCommitQueue
+ include Presentable
self.table_name = 'ci_builds'
@@ -189,4 +190,11 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v
end
end
+
+ # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
+ # They always return `false`.
+ # This method overwrites the autogenerated one to return correct result.
+ def unknown_failure?
+ Gitlab.rails5? ? failure_reason.nil? : super
+ end
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index 4b66725a3e6..22f516a172f 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -27,8 +27,9 @@ module AtomicInternalId
module ClassMethods
def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
before_validation(on: :create) do
- if read_attribute(column).blank?
- scope_attrs = { scope => association(scope).reader }
+ scope_value = association(scope).reader
+ if read_attribute(column).blank? && scope_value
+ scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym
new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 7677891b9ce..13246a774e3 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -31,12 +31,13 @@ module Avatarable
asset_host = ActionController::Base.asset_host
use_asset_host = asset_host.present?
+ use_authentication = respond_to?(:public?) && !public?
# Avatars for private and internal groups and projects require authentication to be viewed,
# which means they can only be served by Rails, on the regular GitLab host.
# If an asset host is configured, we need to return the fully qualified URL
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
- if use_asset_host && respond_to?(:public?) && !public?
+ if use_asset_host && use_authentication
use_asset_host = false
only_path = false
end
@@ -49,6 +50,6 @@ module Avatarable
url_base << gitlab_config.relative_url_root
end
- url_base + avatar.url
+ url_base + avatar.local_url
end
end
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
new file mode 100644
index 00000000000..b32459fdabf
--- /dev/null
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -0,0 +1,54 @@
+module CacheableAttributes
+ extend ActiveSupport::Concern
+
+ included do
+ after_commit { self.class.expire }
+ end
+
+ class_methods do
+ # Can be overriden
+ def current_without_cache
+ last
+ end
+
+ def cache_key
+ "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json".freeze
+ end
+
+ def defaults
+ {}
+ end
+
+ def build_from_defaults(attributes = {})
+ new(defaults.merge(attributes))
+ end
+
+ def cached
+ json_attributes = Rails.cache.read(cache_key)
+ return nil unless json_attributes.present?
+
+ build_from_defaults(JSON.parse(json_attributes))
+ end
+
+ def current
+ cached_record = cached
+ return cached_record if cached_record.present?
+
+ current_without_cache.tap { |current_record| current_record&.cache! }
+ rescue
+ # Fall back to an uncached value if there are any problems (e.g. Redis down)
+ current_without_cache
+ end
+
+ def expire
+ Rails.cache.delete(cache_key)
+ rescue
+ # Gracefully handle when Redis is not available. For example,
+ # omnibus may fail here during gitlab:assets:compile.
+ end
+ end
+
+ def cache!
+ Rails.cache.write(self.class.cache_key, attributes.to_json)
+ end
+end
diff --git a/app/models/concerns/diff_file.rb b/app/models/concerns/diff_file.rb
new file mode 100644
index 00000000000..72332072012
--- /dev/null
+++ b/app/models/concerns/diff_file.rb
@@ -0,0 +1,9 @@
+module DiffFile
+ extend ActiveSupport::Concern
+
+ def to_hash
+ keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
+
+ as_json(only: keys).merge(diff: diff).with_indifferent_access
+ end
+end
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
new file mode 100644
index 00000000000..7ea042c6742
--- /dev/null
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -0,0 +1,91 @@
+##
+# This module is for replacing `dependent: :destroy` and `before_destroy` hooks.
+#
+# In general, `destroy_all` is inefficient because it calls each callback with `DELETE` queries i.e. O(n), whereas,
+# `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,
+# it is difficult to accomplish it.
+#
+# This module defines a format to use `delete_all` and delete associated external data.
+# Here is an exmaple
+#
+# Situation
+# - `Project` has many `Ci::BuildTraceChunk` through `Ci::Build`
+# - `Ci::BuildTraceChunk` stores associated data in Redis, so it relies on `dependent: :destroy` and `before_destroy` for the deletion
+#
+# How to use
+# - Define `use_fast_destroy :build_trace_chunks` in `Project` model.
+# - Define `begin_fast_destroy` and `finalize_fast_destroy(params)` in `Ci::BuildTraceChunk` model.
+# - Use `fast_destroy_all` instead of `destroy` and `destroy_all`
+# - Remove `dependent: :destroy` and `before_destroy` as it's no longer need
+#
+# Expectation
+# - When a project is `destroy`ed, the associated trace_chunks will be deleted by `delete_all`,
+# and the associated data will be removed, too.
+# - When `fast_destroy_all` is called, it also performns as same.
+module FastDestroyAll
+ extend ActiveSupport::Concern
+
+ ForbiddenActionError = Class.new(StandardError)
+
+ included do
+ before_destroy do
+ raise ForbiddenActionError, '`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`'
+ end
+ end
+
+ class_methods do
+ ##
+ # This method delete rows and associated external data efficiently
+ #
+ # This method can replace `destroy` and `destroy_all` without having `after_destroy` hook
+ def fast_destroy_all
+ params = begin_fast_destroy
+
+ delete_all
+
+ finalize_fast_destroy(params)
+ end
+
+ ##
+ # This method returns identifiers to delete associated external data (e.g. file paths, redis keys)
+ #
+ # This method must be defined in fast destroyable model
+ def begin_fast_destroy
+ raise NotImplementedError
+ end
+
+ ##
+ # This method deletes associated external data with the identifiers returned by `begin_fast_destroy`
+ #
+ # This method must be defined in fast destroyable model
+ def finalize_fast_destroy(params)
+ raise NotImplementedError
+ end
+ end
+
+ module Helpers
+ extend ActiveSupport::Concern
+
+ class_methods do
+ ##
+ # This method is to be defined on models which have fast destroyable models as children,
+ # and let us avoid to use `dependent: :destroy` hook
+ def use_fast_destroy(relation)
+ before_destroy(prepend: true) do
+ perform_fast_destroy(public_send(relation)) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+
+ def perform_fast_destroy(subject)
+ params = subject.begin_fast_destroy
+
+ run_after_commit do
+ subject.finalize_fast_destroy(params)
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 01957da0bf3..261ace57a17 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -37,7 +37,20 @@ module GroupDescendant
parent ||= preloaded.detect { |possible_parent| possible_parent.is_a?(Group) && possible_parent.id == child.parent_id }
if parent.nil? && !child.parent_id.nil?
- raise ArgumentError.new('parent was not preloaded')
+ parent = child.parent
+
+ exception = ArgumentError.new <<~MSG
+ parent: [GroupDescendant: #{parent.inspect}] was not preloaded for [#{child.inspect}]")
+ This error is not user facing, but causes a +1 query.
+ MSG
+ extras = {
+ parent: parent,
+ child: child,
+ preloaded: preloaded.map(&:full_path)
+ }
+ issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
+
+ Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras)
end
if parent.nil? && hierarchy_top.present?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index d9416352f9c..b45395343cc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -48,7 +48,7 @@ module Issuable
end
has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :labels, -> { auto_include(false) }, through: :label_links
+ has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 5130ecec472..967fd9c5eea 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -102,14 +102,14 @@ module Milestoneish
Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate)
end
- private
-
def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do
issues_visible_to_user(user).reorder(nil).group(:state).count
end
end
+ private
+
def memoize_per_user(user, method_name)
memoized_users[method_name][user&.id] ||= yield
end
diff --git a/app/models/concerns/nonatomic_internal_id.rb b/app/models/concerns/nonatomic_internal_id.rb
deleted file mode 100644
index 9d0c9b8512f..00000000000
--- a/app/models/concerns/nonatomic_internal_id.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module NonatomicInternalId
- extend ActiveSupport::Concern
-
- included do
- validate :set_iid, on: :create
- validates :iid, presence: true, numericality: true
- end
-
- def set_iid
- if iid.blank?
- parent = project || group
- records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
- max_iid = records.maximum(:iid)
-
- self.iid = max_iid.to_i + 1
- end
- end
-
- def to_param
- iid.to_s
- end
-end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index e48bc0be410..01b1ef9f82c 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -98,6 +98,10 @@ module Participable
participants.merge(ext.users)
+ filter_by_ability(participants)
+ end
+
+ def filter_by_ability(participants)
case self
when PersonalSnippet
Ability.users_that_can_read_personal_snippet(participants.to_a, self)
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 454374121f3..94eef4ff7cd 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -31,7 +31,7 @@ module ProtectedRef
end
end
- def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil)
+ def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.check_access(user)
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 2589215ad19..eef9caf1c8e 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -60,13 +60,16 @@ module ReactiveCaching
end
def with_reactive_cache(*args, &blk)
- within_reactive_cache_lifetime(*args) do
+ bootstrap = !within_reactive_cache_lifetime?(*args)
+ Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
+
+ if bootstrap
+ ReactiveCachingWorker.perform_async(self.class, id, *args)
+ nil
+ else
data = Rails.cache.read(full_reactive_cache_key(*args))
yield data if data.present?
end
- ensure
- Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
- ReactiveCachingWorker.perform_async(self.class, id, *args)
end
def clear_reactive_cache!(*args)
@@ -75,7 +78,7 @@ module ReactiveCaching
def exclusively_update_reactive_cache!(*args)
locking_reactive_cache(*args) do
- within_reactive_cache_lifetime(*args) do
+ if within_reactive_cache_lifetime?(*args)
enqueuing_update(*args) do
value = calculate_reactive_cache(*args)
Rails.cache.write(full_reactive_cache_key(*args), value)
@@ -105,8 +108,8 @@ module ReactiveCaching
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
end
- def within_reactive_cache_lifetime(*args)
- yield if Rails.cache.read(alive_reactive_cache_key(*args))
+ def within_reactive_cache_lifetime?(*args)
+ !!Rails.cache.read(alive_reactive_cache_key(*args))
end
def enqueuing_update(*args)
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index b889f4202dc..b5425295130 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -7,7 +7,11 @@ module RedisCacheable
class_methods do
def cached_attr_reader(*attributes)
attributes.each do |attribute|
- define_method("#{attribute}") do
+ define_method(attribute) do
+ unless self.has_attribute?(attribute)
+ raise ArgumentError, "`cached_attr_reader` requires the #{self.class.name}\##{attribute} attribute to have a database column"
+ end
+
cached_attribute(attribute) || read_attribute(attribute)
end
end
@@ -15,13 +19,16 @@ module RedisCacheable
end
def cached_attribute(attribute)
- (cached_attributes || {})[attribute]
+ cached_value = (cached_attributes || {})[attribute]
+ cast_value_from_cache(attribute, cached_value) if cached_value
end
def cache_attributes(values)
Gitlab::Redis::SharedState.with do |redis|
redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME)
end
+
+ clear_memoization(:cached_attributes)
end
private
@@ -38,4 +45,12 @@ module RedisCacheable
end
end
end
+
+ def cast_value_from_cache(attribute, value)
+ if Gitlab.rails5?
+ self.class.type_for_attribute(attribute).cast(value)
+ else
+ self.class.column_for_attribute(attribute).type_cast_from_database(value)
+ end
+ end
end
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index 399abb67c9d..7c236369793 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -102,7 +102,7 @@ module ResolvableDiscussion
yield(notes_relation)
# Set the notes array to the updated notes
- @notes = notes_relation.fresh.auto_include(false).to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables
self.class.memoized_values.each do |name|
clear_memoization(name)
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 668c5a079e3..4a0f8b92b3a 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -32,7 +32,7 @@ module ResolvableNote
# Keep this method in sync with the `potentially_resolvable` scope
def potentially_resolvable?
- RESOLVABLE_TYPES.include?(self.class.name) && noteable.supports_resolvable_notes?
+ RESOLVABLE_TYPES.include?(self.class.name) && noteable&.supports_resolvable_notes?
end
# Keep this method in sync with the `resolvable` scope
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index dfd7d94450b..0176a12a131 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -4,7 +4,9 @@ module Routable
extend ActiveSupport::Concern
included do
- has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # Remove `inverse_of: source` when upgraded to rails 5.2
+ # See https://github.com/rails/rails/pull/28808
+ has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates :route, presence: true
@@ -102,7 +104,7 @@ module Routable
# the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path
- return uncached_full_path unless RequestStore.active?
+ return uncached_full_path unless RequestStore.active? && persisted?
RequestStore[full_path_key] ||= uncached_full_path
end
@@ -124,6 +126,11 @@ module Routable
end
end
+ # Group would override this to check from association
+ def owned_by?(user)
+ owner == user
+ end
+
private
def set_path_errors
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index 703a72c355c..3796737427a 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -4,18 +4,34 @@ module ShaAttribute
module ClassMethods
def sha_attribute(name)
return if ENV['STATIC_VERIFICATION']
- return unless table_exists?
+
+ validate_binary_column_exists!(name) unless Rails.env.production?
+
+ attribute(name, Gitlab::Database::ShaAttribute.new)
+ end
+
+ # This only gets executed in non-production environments as an additional check to ensure
+ # the column is the correct type. In production it should behave like any other attribute.
+ # See https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5502 for more discussion
+ def validate_binary_column_exists!(name)
+ unless table_exists?
+ warn "WARNING: sha_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations"
+ return
+ end
column = columns.find { |c| c.name == name.to_s }
- # In case the table doesn't exist we won't be able to find the column,
- # thus we will only check the type if the column is present.
- if column && column.type != :binary
- raise ArgumentError,
- "sha_attribute #{name.inspect} is invalid since the column type is not :binary"
+ unless column
+ warn "WARNING: sha_attribute #{name.inspect} is invalid since the column doesn't exist - you may need to run database migrations"
+ return
end
- attribute(name, Gitlab::Database::ShaAttribute.new)
+ unless column.type == :binary
+ raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column type is not :binary")
+ end
+ rescue => error
+ Gitlab::AppLogger.error "ShaAttribute initialization: #{error.message}"
+ raise
end
end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index cefa5c13c5f..db7254c27e0 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -12,8 +12,8 @@ module Sortable
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
- scope :order_name_asc, -> { reorder(name: :asc) }
- scope :order_name_desc, -> { reorder(name: :desc) }
+ scope :order_name_asc, -> { reorder("lower(name) asc") }
+ scope :order_name_desc, -> { reorder("lower(name) desc") }
end
module ClassMethods
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index f05e606995d..f66bdd529f1 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -45,25 +45,25 @@ module Storage
# Hooks
- # Save the storage paths before the projects are destroyed to use them on after destroy
+ # Save the storages before the projects are destroyed to use them on after destroy
def prepare_for_destroy
- old_repository_storage_paths
+ old_repository_storages
end
private
def move_repositories
- # Move the namespace directory in all storage paths used by member projects
- repository_storage_paths.each do |repository_storage_path|
+ # Move the namespace directory in all storages used by member projects
+ repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, full_path_was)
+ gitlab_shell.add_namespace(repository_storage, full_path_was)
# Ensure new directory exists before moving it (if there's a parent)
- gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent
+ gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
- unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
+ unless gitlab_shell.mv_namespace(repository_storage, full_path_was, full_path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
+ Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
@@ -72,33 +72,33 @@ module Storage
end
end
- def old_repository_storage_paths
- @old_repository_storage_paths ||= repository_storage_paths
+ def old_repository_storages
+ @old_repository_storage_paths ||= repository_storages
end
- def repository_storage_paths
+ def repository_storages
# We need to get the storage paths for all the projects, even the ones that are
# pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT.
Project.unscoped do
- all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
+ all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage)
end
end
def rm_dir
# Remove the namespace directory in all storages paths used by member projects
- old_repository_storage_paths.each do |repository_storage_path|
+ old_repository_storages.each do |repository_storage|
# Move namespace directory into trash.
# We will remove it later async
new_path = "#{full_path}+#{id}+deleted"
- if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path)
+ 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
# GitLab has time to remove all projects first
run_after_commit do
- GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
+ GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
end
end
end
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 5911b56c34c..1caf47072bc 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -30,6 +30,8 @@ module TimeTrackable
return if @time_spent == 0
+ touch if touchable?
+
if @time_spent == :reset
reset_spent_time
else
@@ -51,8 +53,16 @@ module TimeTrackable
Gitlab::TimeTrackingFormatter.output(time_estimate)
end
+ def time_estimate=(val)
+ val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
+ end
+
private
+ def touchable?
+ valid? && persisted?
+ end
+
def reset_spent_time
timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/models/concerns/uniquify.rb b/app/models/concerns/uniquify.rb
index a7fe5951b6e..549a76da20e 100644
--- a/app/models/concerns/uniquify.rb
+++ b/app/models/concerns/uniquify.rb
@@ -1,13 +1,21 @@
+# Uniquify
+#
+# Return a version of the given 'base' string that is unique
+# by appending a counter to it. Uniqueness is determined by
+# repeated calls to the passed block.
+#
+# You can pass an initial value for the counter, if not given
+# counting starts from 1.
+#
+# If `base` is a function/proc, we expect that calling it with a
+# candidate counter returns a string to test/return.
class Uniquify
- # Return a version of the given 'base' string that is unique
- # by appending a counter to it. Uniqueness is determined by
- # repeated calls to the passed block.
- #
- # If `base` is a function/proc, we expect that calling it with a
- # candidate counter returns a string to test/return.
+ def initialize(counter = nil)
+ @counter = counter
+ end
+
def string(base)
@base = base
- @counter = nil
increment_counter! while yield(base_string)
base_string
diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb
new file mode 100644
index 00000000000..e7cfffb775b
--- /dev/null
+++ b/app/models/concerns/with_uploads.rb
@@ -0,0 +1,39 @@
+# 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
+# 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
+#
+# And because not-mounted uploads require presence of upload's
+# object model when destroying them (FileUploader's `build_upload` method
+# references `model` on delete), we can not use after_commit hook for these
+# uploads.
+#
+# Instead FileUploads are destroyed in before_destroy hook and remaining uploads
+# are destroyed by the carrierwave's after_commit hook.
+
+module WithUploads
+ extend ActiveSupport::Concern
+
+ # Currently there is no simple way how to select only not-mounted
+ # uploads, it should be all FileUploaders so we select them by
+ # `uploader` class
+ FILE_UPLOADERS = %w(PersonalFileUploader NamespaceFileUploader FileUploader).freeze
+
+ included do
+ has_many :uploads, as: :model
+
+ before_destroy :destroy_file_uploads
+ end
+
+ # mounted uploads are deleted in carrierwave's after_commit hook,
+ # but FileUploaders which are not mounted must be deleted explicitly and
+ # it can not be done in after_commit because FileUploader requires loads
+ # associated model on destroy (which is already deleted in after_commit)
+ def destroy_file_uploads
+ self.uploads.where(uploader: FILE_UPLOADERS).find_each do |upload|
+ upload.destroy
+ end
+ end
+end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 858b7ef533e..89a74b7dcb1 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -2,7 +2,7 @@ class DeployKey < Key
include IgnorableColumn
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :deploy_keys_projects
+ has_many :projects, through: :deploy_keys_projects
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
scope :are_public, -> { where(public: true) }
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 8dae821a10e..5082dc45368 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -4,11 +4,12 @@ class DeployToken < ActiveRecord::Base
add_authentication_token_field :token
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
+ GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze
default_value_for(:expires_at) { Forever.date }
has_many :project_deploy_tokens, inverse_of: :deploy_token
- has_many :projects, -> { auto_include(false) }, through: :project_deploy_tokens
+ has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
before_save :ensure_token
@@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base
scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
+ def self.gitlab_deploy_token
+ active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
+ end
+
def revoke!
update!(revoked: true)
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index e18ea8bfea4..254764eefde 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -1,11 +1,13 @@
class Deployment < ActiveRecord::Base
- include NonatomicInternalId
+ include AtomicInternalId
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) }
+
validates :sha, presence: true
validates :ref, presence: true
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 15122cbc693..d752d5bcdee 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -3,6 +3,7 @@
# A note of this type can be resolvable.
class DiffNote < Note
include NoteOnDiff
+ include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
@@ -12,7 +13,6 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
- validates :diff_line, presence: true, if: :on_text?
validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
@@ -23,6 +23,7 @@ class DiffNote < Note
before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits
+ after_commit :create_diff_file, on: :create
def discussion_class(*)
DiffDiscussion
@@ -53,8 +54,25 @@ class DiffNote < Note
position.position_type == "image"
end
+ def create_diff_file
+ return unless should_create_diff_file?
+
+ diff_file = fetch_diff_file
+ diff_line = diff_file.line_for_position(self.original_position)
+
+ creation_params = diff_file.diff.to_hash
+ .except(:too_large)
+ .merge(diff: diff_file.diff_hunk(diff_line))
+
+ create_note_diff_file(creation_params)
+ end
+
def diff_file
- @diff_file ||= self.original_position.diff_file(self.project.repository)
+ strong_memoize(:diff_file) do
+ enqueue_diff_file_creation_job if should_create_diff_file?
+
+ fetch_diff_file
+ end
end
def diff_line
@@ -85,6 +103,38 @@ class DiffNote < Note
private
+ def enqueue_diff_file_creation_job
+ # Avoid enqueuing multiple file creation jobs at once for a note (i.e.
+ # parallel calls to `DiffNote#diff_file`).
+ lease = Gitlab::ExclusiveLease.new("note_diff_file_creation:#{id}", timeout: 1.hour.to_i)
+ return unless lease.try_obtain
+
+ CreateNoteDiffFileWorker.perform_async(id)
+ end
+
+ def should_create_diff_file?
+ on_text? && note_diff_file.nil? && self == 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(paths: original_position.paths, expanded: true).diff_files.first
+ else
+ original_position.diff_file(self.project.repository)
+ end
+ end
+
def supported?
for_commit? || self.noteable.has_complete_diff_refs?
end
diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb
index aad3509b895..7f1728e8c77 100644
--- a/app/models/fork_network.rb
+++ b/app/models/fork_network.rb
@@ -1,7 +1,7 @@
class ForkNetwork < ActiveRecord::Base
belongs_to :root_project, class_name: 'Project'
has_many :fork_network_members
- has_many :projects, -> { auto_include(false) }, through: :fork_network_members
+ has_many :projects, through: :fork_network_members
after_create :add_root_as_member, if: :root_project
diff --git a/app/models/group.rb b/app/models/group.rb
index 202988d743d..8fb77a7869d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -9,12 +9,14 @@ class Group < Namespace
include SelectForProjectAuthorization
include LoadedInGroupList
include GroupDescendant
+ include TokenAuthenticatable
+ include WithUploads
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
- has_many :users, -> { auto_include(false) }, through: :group_members
+ has_many :users, through: :group_members
has_many :owners,
- -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) },
+ -> { where(members: { access_level: Gitlab::Access::OWNER }) },
through: :group_members,
source: :user
@@ -23,14 +25,12 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :shared_projects, -> { auto_include(false) }, through: :project_group_links, source: :project
+ has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
has_many :boards
has_many :badges, class_name: 'GroupBadge'
@@ -43,6 +43,8 @@ class Group < Namespace
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ add_authentication_token_field :runners_token
+
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
@@ -125,6 +127,10 @@ class Group < Namespace
self[:lfs_enabled]
end
+ def owned_by?(user)
+ owners.include?(user)
+ end
+
def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users(
self,
@@ -234,6 +240,13 @@ class Group < Namespace
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end
+ # Returns all members that are part of the group, it's subgroups, and ancestor groups
+ def direct_and_indirect_members
+ GroupMember
+ .active_without_invites_and_requests
+ .where(source_id: self_and_hierarchy.reorder(nil).select(:id))
+ end
+
def users_with_parents
User
.where(id: members_with_parents.select(:user_id))
@@ -246,6 +259,30 @@ class Group < Namespace
.reorder(nil)
end
+ # Returns all users that are members of the group because:
+ # 1. They belong to the group
+ # 2. They belong to a project that belongs to the group
+ # 3. They belong to a sub-group or project in such sub-group
+ # 4. They belong to an ancestor group
+ def direct_and_indirect_users
+ union = Gitlab::SQL::Union.new([
+ User
+ .where(id: direct_and_indirect_members.select(:user_id))
+ .reorder(nil),
+ project_users_with_descendants
+ ])
+
+ User.from("(#{union.to_sql}) #{User.table_name}")
+ end
+
+ # Returns all users that are members of projects
+ # belonging to the current group or sub-groups
+ def project_users_with_descendants
+ User
+ .joins(projects: :group)
+ .where(namespaces: { id: self_and_descendants.select(:id) })
+ end
+
def max_member_access_for_user(user)
return GroupMember::OWNER if user.admin?
@@ -290,6 +327,13 @@ class Group < Namespace
refresh_members_authorized_projects(blocking: false)
end
+ # each existing group needs to have a `runners_token`.
+ # we do this on read since migrating all existing groups is not a feasible
+ # solution.
+ def runners_token
+ ensure_runners_token!
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 1011b9f1109..3fd0c5e751d 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -1,12 +1,16 @@
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: :provider, case_sensitive: false }
- validates :user_id, uniqueness: { scope: :provider }
+ validates :extern_uid, allow_blank: true, uniqueness: { scope: uniqueness_scope, case_sensitive: false }
+ validates :user_id, uniqueness: { scope: uniqueness_scope }
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/internal_id.rb b/app/models/internal_id.rb
index 96a43006642..189942c5ad8 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -12,8 +12,9 @@
# * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ActiveRecord::Base
belongs_to :project
+ belongs_to :namespace
- enum usage: { issues: 0 }
+ enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
validates :usage, presence: true
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 702bfc77803..0332bfa9371 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -34,7 +34,7 @@ class Issue < ActiveRecord::Base
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assignees, -> { auto_include(false) }, class_name: "User", through: :issue_assignees
+ has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true
@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request
end
+ def suggested_branch_name
+ return to_branch_name unless project.repository.branch_exists?(to_branch_name)
+
+ start_counting_from = 2
+ Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
+ project.repository.branch_exists?(suggested_branch_name)
+ end
+ end
+
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def has_related_branch?
@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base
end
end
- def can_be_worked_on?(current_user)
- !self.closed? &&
- !self.project.forked? &&
- self.related_branches(current_user).empty? &&
- self.closed_by_merge_requests(current_user).empty?
+ def can_be_worked_on?
+ !self.closed? && !self.project.forked?
end
# Returns `true` if the current issue can be viewed by either a logged in User
diff --git a/app/models/label.rb b/app/models/label.rb
index 12e8c5695d4..1cf04976602 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -18,8 +18,8 @@ class Label < ActiveRecord::Base
has_many :lists, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :issues, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'Issue'
- has_many :merge_requests, -> { auto_include(false) }, through: :label_links, source: :target, source_type: 'MergeRequest'
+ has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
+ has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title_and_color
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index ed95613ee59..84487031ee5 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -3,7 +3,7 @@ class LfsObject < ActiveRecord::Base
include ObjectStorage::BackgroundMove
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :projects, -> { auto_include(false) }, through: :lfs_objects_projects
+ has_many :projects, through: :lfs_objects_projects
scope :with_files_stored_locally, -> { where(file_store: [nil, LfsObjectUploader::Store::LOCAL]) }
@@ -11,10 +11,12 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
- before_save :update_file_store
+ after_save :update_file_store, if: :file_changed?
def update_file_store
- self.file_store = file.object_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
end
def project_allowed_access?(project)
diff --git a/app/models/list.rb b/app/models/list.rb
index 918275be142..5daf35ef845 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -31,7 +31,8 @@ class List < ActiveRecord::Base
if options.key?(:label)
json[:label] = label.as_json(
project: board.project,
- only: [:id, :title, :description, :color]
+ only: [:id, :title, :description, :color],
+ methods: [:text_color]
)
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index eac4a22a03f..68572f2e33a 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -96,6 +96,17 @@ class Member < ActiveRecord::Base
joins(:user).merge(User.search(query))
end
+ def filter_by_2fa(value)
+ case value
+ when 'enabled'
+ left_join_users.merge(User.with_two_factor_indistinct)
+ when 'disabled'
+ left_join_users.merge(User.without_two_factor)
+ else
+ all
+ end
+ end
+
def sort_by_attribute(method)
case method.to_s
when 'access_level_asc' then reorder(access_level: :asc)
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 661e668dbf9..5da739f9618 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -37,20 +37,20 @@ class GroupMember < Member
private
def send_invite
- notification_service.invite_group_member(self, @raw_invite_token)
+ run_after_commit_or_now { notification_service.invite_group_member(self, @raw_invite_token) }
super
end
def post_create_hook
- notification_service.new_group_member(self)
+ run_after_commit_or_now { notification_service.new_group_member(self) }
super
end
def post_update_hook
if access_level_changed?
- notification_service.update_group_member(self)
+ run_after_commit { notification_service.update_group_member(self) }
end
super
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 1c7ed4a96df..024106056b4 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -92,7 +92,7 @@ class ProjectMember < Member
private
def send_invite
- notification_service.invite_project_member(self, @raw_invite_token)
+ run_after_commit_or_now { notification_service.invite_project_member(self, @raw_invite_token) }
super
end
@@ -100,7 +100,7 @@ class ProjectMember < Member
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
- notification_service.new_project_member(self)
+ run_after_commit_or_now { notification_service.new_project_member(self) }
end
super
@@ -108,7 +108,7 @@ class ProjectMember < Member
def post_update_hook
if access_level_changed?
- notification_service.update_project_member(self)
+ run_after_commit { notification_service.update_project_member(self) }
end
super
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 91d8be5559b..9c4384a6e42 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,5 +1,5 @@
class MergeRequest < ActiveRecord::Base
- include NonatomicInternalId
+ include AtomicInternalId
include Issuable
include Noteable
include Referable
@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
+ has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
+
has_many :merge_request_diffs
has_one :merge_request_diff,
@@ -102,24 +104,35 @@ class MergeRequest < ActiveRecord::Base
state_machine :merge_status, initial: :unchecked do
event :mark_as_unchecked do
- transition [:can_be_merged, :cannot_be_merged] => :unchecked
+ transition [:can_be_merged, :unchecked] => :unchecked
+ transition [:cannot_be_merged, :cannot_be_merged_recheck] => :cannot_be_merged_recheck
end
event :mark_as_mergeable do
- transition [:unchecked, :cannot_be_merged] => :can_be_merged
+ transition [:unchecked, :cannot_be_merged_recheck] => :can_be_merged
end
event :mark_as_unmergeable do
- transition [:unchecked, :can_be_merged] => :cannot_be_merged
+ transition [:unchecked, :cannot_be_merged_recheck] => :cannot_be_merged
end
state :unchecked
+ state :cannot_be_merged_recheck
state :can_be_merged
state :cannot_be_merged
around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block)
end
+
+ after_transition unchecked: :cannot_be_merged do |merge_request, transition|
+ NotificationService.new.merge_request_unmergeable(merge_request)
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
+
+ def check_state?(merge_status)
+ [:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym)
+ end
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
@@ -321,10 +334,20 @@ class MergeRequest < ActiveRecord::Base
# updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params)
- jid = MergeWorker.perform_async(id, user_id, params)
+ jid = MergeWorker.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid)
end
+ def merge_participants
+ participants = [author]
+
+ if merge_when_pipeline_succeeds? && !participants.include?(merge_user)
+ participants << merge_user
+ end
+
+ participants
+ end
+
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
@@ -600,7 +623,7 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
- return unless unchecked? && Gitlab::Database.read_write?
+ return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write?
can_be_merged =
!broken? && project.repository.can_be_merged?(diff_head_sha, target_branch)
@@ -1005,6 +1028,10 @@ class MergeRequest < ActiveRecord::Base
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
+ def short_merge_commit_sha
+ Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
+ end
+
def can_be_reverted?(current_user)
return false unless merge_commit
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c1c27ccf3e5..06aa67c600f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end
- def commits_count
- super || merge_request_diff_commits.size
- end
-
private
def create_merge_request_diff_files(diffs)
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 1199ff5af22..cd8ba6b904d 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -1,5 +1,6 @@
class MergeRequestDiffFile < ActiveRecord::Base
include Gitlab::EncodingHelper
+ include DiffFile
belongs_to :merge_request_diff
@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base
def diff
binary? ? super.unpack('m0').first : super
end
-
- def to_hash
- keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
-
- as_json(only: keys).merge(diff: diff).with_indifferent_access
- end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 8e33bab81c1..d14e3a4ded5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
- include NonatomicInternalId
+ include AtomicInternalId
include Sortable
include Referable
include StripAttribute
@@ -21,8 +21,11 @@ class Milestone < ActiveRecord::Base
belongs_to :project
belongs_to :group
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
+ has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
+
has_many :issues
- has_many :labels, -> { distinct.reorder('labels.title').auto_include(false) }, through: :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 2b63aa33222..3dad4277713 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -21,6 +21,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
+ has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace'
+ has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
+
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: "User"
@@ -163,6 +166,13 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any?
end
+ # Returns all ancestors, self, and descendants of the current namespace.
+ def self_and_hierarchy
+ Gitlab::GroupHierarchy
+ .new(self.class.where(id: id))
+ .all_groups
+ end
+
# Returns all the ancestors of the current namespaces.
def ancestors
return self.class.none unless parent_id
@@ -248,10 +258,6 @@ class Namespace < ActiveRecord::Base
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
- def features
- []
- end
-
def refresh_project_authorizations
owner.refresh_authorized_projects
end
diff --git a/app/models/note.rb b/app/models/note.rb
index e426f84832b..02f7a9b1e4f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base
has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
+ has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do
- includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
+ includes(:project, :author, :updated_by, :resolved_by, :award_emoji,
+ :system_note_metadata, :note_diff_file)
end
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
@@ -317,10 +319,6 @@ class Note < ActiveRecord::Base
!system? && !for_snippet?
end
- def can_create_notification?
- true
- end
-
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
new file mode 100644
index 00000000000..e688018a6d9
--- /dev/null
+++ b/app/models/note_diff_file.rb
@@ -0,0 +1,7 @@
+class NoteDiffFile < ActiveRecord::Base
+ include DiffFile
+
+ belongs_to :diff_note, inverse_of: :note_diff_file
+
+ validates :diff_note, presence: true
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 38139a9b137..0fe9f8880b4 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -22,6 +22,8 @@ class Project < ActiveRecord::Base
include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize
include ChronicDurationAttribute
+ include FastDestroyAll::Helpers
+ include WithUploads
extend Gitlab::ConfigHelper
@@ -64,15 +66,29 @@ class Project < ActiveRecord::Base
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
add_authentication_token_field :runners_token
+
+ before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) }
+
before_save :ensure_runners_token
after_save :update_project_statistics, if: :namespace_id_changed?
+
+ after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
+
after_create :create_project_feature, unless: :project_feature
+
+ after_create :create_ci_cd_settings,
+ unless: :ci_cd_settings,
+ if: proc { ProjectCiCdSetting.available? }
+
after_create :set_last_activity_at
after_create :set_last_repository_updated_at
after_update :update_forks_visibility_level
before_destroy :remove_private_deploy_keys
+
+ use_fast_destroy :build_trace_chunks
+
after_destroy -> { run_after_commit { remove_pages } }
after_destroy :remove_exports
@@ -138,11 +154,11 @@ class Project < ActiveRecord::Base
has_one :packagist_service
# TODO: replace these relations with the fork network versions
- has_one :forked_project_link, foreign_key: "forked_to_project_id"
- has_one :forked_from_project, -> { auto_include(false) }, through: :forked_project_link
+ has_one :forked_project_link, foreign_key: "forked_to_project_id"
+ has_one :forked_from_project, through: :forked_project_link
has_many :forked_project_links, foreign_key: "forked_from_project_id"
- has_many :forks, -> { auto_include(false) }, through: :forked_project_links, source: :forked_to_project
+ has_many :forks, through: :forked_project_links, source: :forked_to_project
# TODO: replace these relations with the fork network versions
has_one :root_of_fork_network,
@@ -150,7 +166,9 @@ class Project < ActiveRecord::Base
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
- has_one :fork_network, -> { auto_include(false) }, through: :fork_network_member
+ has_one :fork_network, through: :fork_network_member
+
+ has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -167,27 +185,27 @@ class Project < ActiveRecord::Base
has_many :protected_tags
has_many :project_authorizations
- has_many :authorized_users, -> { auto_include(false) }, through: :project_authorizations, source: :user, class_name: 'User'
+ has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
- has_many :users, -> { auto_include(false) }, through: :project_members
+ has_many :users, through: :project_members
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
- has_many :deploy_keys, -> { auto_include(false) }, through: :deploy_keys_projects
+ has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects
- has_many :starrers, -> { auto_include(false) }, through: :users_star_projects, source: :user
+ has_many :starrers, through: :users_star_projects, source: :user
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :lfs_objects, -> { auto_include(false) }, through: :lfs_objects_projects
+ has_many :lfs_objects, through: :lfs_objects_projects
has_many :lfs_file_locks
has_many :project_group_links
- has_many :invited_groups, -> { auto_include(false) }, through: :project_group_links, source: :group
+ has_many :invited_groups, through: :project_group_links, source: :group
has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
@@ -199,7 +217,8 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
- has_many :clusters, -> { auto_include(false) }, through: :cluster_project, class_name: 'Clusters::Cluster'
+ has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
+ has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -215,32 +234,39 @@ class Project < ActiveRecord::Base
# still using `dependent: :destroy` here.
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
+ has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :runner_projects, class_name: 'Ci::RunnerProject'
- has_many :runners, -> { auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
- has_many :deploy_tokens, -> { auto_include(false) }, through: :project_deploy_tokens
-
- has_many :active_runners, -> { active.auto_include(false) }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_many :deploy_tokens, through: :project_deploy_tokens
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge'
+ has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ has_many :remote_mirrors, inverse_of: :project
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
+ accepts_nested_attributes_for :remote_mirrors,
+ allow_destroy: true,
+ reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
+
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
+ delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
# Validations
validates :creator, presence: true, on: :create
@@ -276,8 +302,6 @@ class Project < ActiveRecord::Base
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
validates :variables, variable_duplicates: { scope: :environment_scope }
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
@@ -325,6 +349,12 @@ class Project < ActiveRecord::Base
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
+ scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
+
+ scope :with_group_runners_enabled, -> do
+ joins(:ci_cd_settings)
+ .where(project_ci_cd_settings: { group_runners_enabled: true })
+ end
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
@@ -375,55 +405,9 @@ class Project < ActiveRecord::Base
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
scope :excluding_project, ->(project) { where.not(id: project) }
- scope :import_started, -> { where(import_status: 'started') }
-
- state_machine :import_status, initial: :none do
- event :import_schedule do
- transition [:none, :finished, :failed] => :scheduled
- end
-
- event :force_import_start do
- transition [:none, :finished, :failed] => :started
- end
-
- event :import_start do
- transition scheduled: :started
- end
-
- event :import_finish do
- transition started: :finished
- end
-
- event :import_fail do
- transition [:scheduled, :started] => :failed
- end
-
- event :import_retry do
- transition failed: :started
- end
-
- state :scheduled
- state :started
- state :finished
- state :failed
-
- after_transition [:none, :finished, :failed] => :scheduled do |project, _|
- project.run_after_commit do
- job_id = add_import_job
- update(import_jid: job_id) if job_id
- end
- end
- after_transition started: :finished do |project, _|
- project.reset_cache_and_import_attrs
-
- if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
- project.run_after_commit do
- Projects::AfterImportService.new(project).execute
- end
- end
- end
- end
+ scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
+ scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") }
class << self
# Searches for a list of projects based on the query given in `query`.
@@ -512,10 +496,6 @@ class Project < ActiveRecord::Base
repository.empty?
end
- def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]&.legacy_disk_path
- end
-
def team
@team ||= ProjectTeam.new(self)
end
@@ -657,10 +637,6 @@ class Project < ActiveRecord::Base
external_import? || forked? || gitlab_project_import? || bare_repository_import?
end
- def no_import?
- import_status == 'none'
- end
-
def external_import?
import_url.present?
end
@@ -673,6 +649,99 @@ class Project < ActiveRecord::Base
import_started? || import_scheduled?
end
+ def import_state_args
+ {
+ status: self[:import_status],
+ jid: self[:import_jid],
+ last_error: self[:import_error]
+ }
+ end
+
+ def ensure_import_state(force: false)
+ return if !force && (self[:import_status] == 'none' || self[:import_status].nil?)
+ return unless import_state.nil?
+
+ if persisted?
+ create_import_state(import_state_args)
+
+ update_column(:import_status, 'none')
+ else
+ build_import_state(import_state_args)
+
+ self[:import_status] = 'none'
+ end
+ end
+
+ def import_schedule
+ ensure_import_state(force: true)
+
+ import_state.schedule
+ end
+
+ def force_import_start
+ ensure_import_state(force: true)
+
+ import_state.force_start
+ end
+
+ def import_start
+ ensure_import_state(force: true)
+
+ import_state.start
+ end
+
+ def import_fail
+ ensure_import_state(force: true)
+
+ import_state.fail_op
+ end
+
+ def import_finish
+ ensure_import_state(force: true)
+
+ import_state.finish
+ end
+
+ def import_jid=(new_jid)
+ ensure_import_state(force: true)
+
+ import_state.jid = new_jid
+ end
+
+ def import_jid
+ ensure_import_state
+
+ import_state&.jid
+ end
+
+ def import_error=(new_error)
+ ensure_import_state(force: true)
+
+ import_state.last_error = new_error
+ end
+
+ def import_error
+ ensure_import_state
+
+ import_state&.last_error
+ end
+
+ def import_status=(new_status)
+ ensure_import_state(force: true)
+
+ import_state.status = new_status
+ end
+
+ def import_status
+ ensure_import_state
+
+ import_state&.status || 'none'
+ end
+
+ def no_import?
+ import_status == 'none'
+ end
+
def import_started?
# import? does SQL work so only run it if it looks like there's an import running
import_status == 'started' && import?
@@ -706,6 +775,37 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
+ def has_remote_mirror?
+ remote_mirror_available? && remote_mirrors.enabled.exists?
+ end
+
+ def updating_remote_mirror?
+ remote_mirrors.enabled.started.exists?
+ end
+
+ def update_remote_mirrors
+ return unless remote_mirror_available?
+
+ remote_mirrors.enabled.each(&:sync)
+ end
+
+ def mark_stuck_remote_mirrors_as_failed!
+ remote_mirrors.stuck.update_all(
+ update_status: :failed,
+ last_error: 'The remote mirror took to long to complete.',
+ last_update_at: Time.now
+ )
+ end
+
+ def mark_remote_mirrors_for_removal
+ remote_mirrors.each(&:mark_for_delete_if_blank_url)
+ end
+
+ def remote_mirror_available?
+ remote_mirror_available_overridden ||
+ ::Gitlab::CurrentSettings.mirror_available
+ end
+
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
@@ -794,6 +894,13 @@ class Project < ActiveRecord::Base
Gitlab::Routing.url_helpers.project_url(self)
end
+ def readme_url
+ readme = repository.readme
+ if readme
+ Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme.path))
+ end
+ end
+
def new_issuable_address(author, address_type)
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
@@ -897,7 +1004,7 @@ class Project < ActiveRecord::Base
available_services_names = Service.available_services_names - exceptions
- available_services_names.map do |service_name|
+ available_services = available_services_names.map do |service_name|
service = find_service(services, service_name)
if service
@@ -914,6 +1021,14 @@ class Project < ActiveRecord::Base
end
end
end
+
+ available_services.reject do |service|
+ disabled_services.include?(service.to_param)
+ end
+ end
+
+ def disabled_services
+ []
end
def find_or_initialize_service(name)
@@ -1041,13 +1156,6 @@ class Project < ActiveRecord::Base
"#{web_url}.git"
end
- def user_can_push_to_empty_repo?(user)
- return false unless empty_repo?
- return false unless Ability.allowed?(user, :push_code, self)
-
- !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
- end
-
def forked?
return true if fork_network && fork_network.root_project != self
@@ -1106,7 +1214,7 @@ class Project < ActiveRecord::Base
# Check if repository already exists on disk
def check_repository_path_availability
return true if skip_disk_validation
- return false unless repository_storage_path
+ return false unless repository_storage
expires_full_path_cache # we need to clear cache to validate renames correctly
@@ -1306,20 +1414,25 @@ class Project < ActiveRecord::Base
@shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
end
- def active_shared_runners
- @active_shared_runners ||= shared_runners.active
+ def group_runners
+ @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
+ end
+
+ def all_runners
+ union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners])
+ Ci::Runner.from("(#{union.to_sql}) ci_runners")
end
def any_runners?(&block)
- active_runners.any?(&block) || active_shared_runners.any?(&block)
+ all_runners.active.any?(&block)
end
def valid_runners_token?(token)
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
end
- def open_issues_count
- Projects::OpenIssuesCountService.new(self).count
+ def open_issues_count(current_user = nil)
+ Projects::OpenIssuesCountService.new(self, current_user).count
end
def open_merge_requests_count
@@ -1476,7 +1589,7 @@ class Project < ActiveRecord::Base
def rename_repo_notify!
# When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
- send_move_instructions(full_path_was) unless started?
+ send_move_instructions(full_path_was) unless import_started?
expires_full_path_cache
self.old_path_with_namespace = full_path_was
@@ -1530,7 +1643,8 @@ class Project < ActiveRecord::Base
return unless import_jid
Gitlab::SidekiqStatus.unset(import_jid)
- update_column(:import_jid, nil)
+
+ import_state.update_column(:jid, nil)
end
def running_or_pending_build_count(force: false)
@@ -1549,7 +1663,8 @@ class Project < ActiveRecord::Base
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
import_fail
- update_column(:import_error, sanitized_message)
+
+ import_state.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
@@ -1875,6 +1990,18 @@ class Project < ActiveRecord::Base
memoized_results[cache_key]
end
+ def licensed_features
+ []
+ end
+
+ def toggle_ci_cd_settings!(settings_attribute)
+ ci_cd_settings.toggle!(settings_attribute)
+ end
+
+ def gitlab_deploy_token
+ @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
+ end
+
private
def storage
@@ -1903,14 +2030,14 @@ class Project < ActiveRecord::Base
def check_repository_absence!
return if skip_disk_validation
- if repository_storage_path.blank? || repository_with_same_path_already_exists?
+ if repository_storage.blank? || repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk')
throw :abort
end
end
def repository_with_same_path_already_exists?
- gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
+ gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
end
# set last_activity_at to the same as created_at
@@ -2000,10 +2127,11 @@ class Project < ActiveRecord::Base
def fetch_branch_allows_maintainer_push?(user, branch_name)
check_access = -> do
+ next false if empty_repo?
+
merge_request = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true)
.find_by(source_branch: branch_name)
-
merge_request&.can_be_merged_by?(user)
end
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
new file mode 100644
index 00000000000..588cced5781
--- /dev/null
+++ b/app/models/project_ci_cd_setting.rb
@@ -0,0 +1,16 @@
+class ProjectCiCdSetting < ActiveRecord::Base
+ belongs_to :project, inverse_of: :ci_cd_settings
+
+ # The version of the schema that first introduced this model/table.
+ MINIMUM_SCHEMA_VERSION = 20180403035759
+
+ def self.available?
+ @available ||=
+ ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
+ end
+
+ def self.reset_column_information
+ @available = nil
+ super
+ end
+end
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
new file mode 100644
index 00000000000..1605317ae14
--- /dev/null
+++ b/app/models/project_import_state.rb
@@ -0,0 +1,55 @@
+class ProjectImportState < ActiveRecord::Base
+ include AfterCommitQueue
+
+ self.table_name = "project_mirror_data"
+
+ belongs_to :project, inverse_of: :import_state
+
+ validates :project, presence: true
+
+ state_machine :status, initial: :none do
+ event :schedule do
+ transition [:none, :finished, :failed] => :scheduled
+ end
+
+ event :force_start do
+ transition [:none, :finished, :failed] => :started
+ end
+
+ event :start do
+ transition scheduled: :started
+ end
+
+ event :finish do
+ transition started: :finished
+ end
+
+ event :fail_op do
+ transition [:scheduled, :started] => :failed
+ end
+
+ state :scheduled
+ state :started
+ state :finished
+ state :failed
+
+ after_transition [:none, :finished, :failed] => :scheduled do |state, _|
+ state.run_after_commit do
+ job_id = project.add_import_job
+ update(jid: job_id) if job_id
+ end
+ end
+
+ after_transition started: :finished do |state, _|
+ project = state.project
+
+ project.reset_cache_and_import_attrs
+
+ if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
+ state.run_after_commit do
+ Projects::AfterImportService.new(project).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 71b10fc6bc1..a4bf427ac0b 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -115,6 +115,6 @@ class DroneCiService < CiService
def merge_request_valid?(data)
data[:object_attributes][:state] == 'opened' &&
- data[:object_attributes][:merge_status] == 'unchecked'
+ MergeRequest.state_machines[:merge_status].check_state?(data[:object_attributes][:merge_status])
end
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 4d23a17a545..da01ac1b7cf 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -1,5 +1,51 @@
require "flowdock-git-hook"
+# Flow dock depends on Grit to compute the number of commits between two given
+# commits. To make this depend on Gitaly, a monkey patch is applied
+module Flowdock
+ class Git
+ # pass down a Repository all the way down
+ def repo
+ @options[:repo]
+ end
+
+ def config
+ {}
+ end
+
+ def messages
+ Git::Builder.new(repo: repo,
+ ref: @ref,
+ before: @from,
+ after: @to,
+ commit_url: @commit_url,
+ branch_url: @branch_url,
+ diff_url: @diff_url,
+ repo_url: @repo_url,
+ repo_name: @repo_name,
+ permanent_refs: @permanent_refs,
+ tags: tags
+ ).to_hashes
+ end
+
+ class Builder
+ def commits
+ @repo.commits_between(@before, @after).map do |commit|
+ {
+ url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
+ id: commit.sha,
+ message: commit.message,
+ author: {
+ name: commit.author_name,
+ email: commit.author_email
+ }
+ }
+ end
+ end
+ end
+ end
+end
+
class FlowdockService < Service
prop_accessor :token
validates :token, presence: true, if: :activated?
@@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before],
data[:after],
token: token,
- repo: project.repository.path_to_repo,
+ repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 26cbfd784ad..84248f9590b 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -3,6 +3,7 @@ require "gemnasium/gitlab_service"
class GemnasiumService < Service
prop_accessor :token, :api_key
validates :token, :api_key, presence: true, if: :activated?
+ validate :deprecation_validation
def title
'Gemnasium'
@@ -27,6 +28,18 @@ class GemnasiumService < Service
%w(push)
end
+ def deprecated?
+ true
+ end
+
+ def deprecation_message
+ "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available."
+ end
+
+ def deprecation_validation
+ errors[:base] << deprecation_message
+ end
+
def execute(data)
return unless supported_events.include?(data[:object_kind])
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 87a4350f022..5d4e3c34b39 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,15 +4,15 @@ class ProjectStatistics < ActiveRecord::Base
before_save :update_storage_size
- STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size].freeze
- STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
+ COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
+ INCREMENTABLE_COLUMNS = [:build_artifacts_size].freeze
def total_repository_size
repository_size + lfs_objects_size
end
def refresh!(only: nil)
- STATISTICS_COLUMNS.each do |column, generator|
+ COLUMNS_TO_REFRESH.each do |column, generator|
if only.blank? || only.include?(column)
public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
end
@@ -34,13 +34,15 @@ class ProjectStatistics < ActiveRecord::Base
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
- def update_build_artifacts_size
- self.build_artifacts_size =
- project.builds.sum(:artifacts_size) +
- Ci::JobArtifact.artifacts_size_for(self.project)
+ def update_storage_size
+ self.storage_size = repository_size + lfs_objects_size + build_artifacts_size
end
- def update_storage_size
- self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
+ def self.increment_statistic(project_id, key, amount)
+ raise ArgumentError, "Cannot increment attribute: #{key}" unless key.in?(INCREMENTABLE_COLUMNS)
+ return if amount == 0
+
+ where(project_id: project_id)
+ .update_all(["#{key} = COALESCE(#{key}, 0) + (?)", amount])
end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 52e067cb44c..f799a0b4227 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -21,7 +21,7 @@ class ProjectWiki
end
delegate :empty?, to: :pages
- delegate :repository_storage_path, :hashed_storage?, to: :project
+ delegate :repository_storage, :hashed_storage?, to: :project
def path
@project.path + '.wiki'
@@ -179,7 +179,11 @@ class ProjectWiki
def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title)
- Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
+ Gitlab::Git::Wiki::CommitDetails.new(@user.id,
+ @user.username,
+ @user.name,
+ @user.email,
+ commit_message)
end
def default_message(action, title)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 609780c5587..cb361a66591 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push
+ def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
+ # Masters, owners and admins are allowed to create the default branch
+ if default_branch_protected? && project.empty_repo?
+ return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
+ end
+
+ super
+ end
+
# Check if branch name is marked as protected in the system
def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected?
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
new file mode 100644
index 00000000000..bbf8fd9c6a7
--- /dev/null
+++ b/app/models/remote_mirror.rb
@@ -0,0 +1,219 @@
+class RemoteMirror < ActiveRecord::Base
+ include AfterCommitQueue
+
+ PROTECTED_BACKOFF_DELAY = 1.minute
+ UNPROTECTED_BACKOFF_DELAY = 5.minutes
+
+ attr_encrypted :credentials,
+ key: Gitlab::Application.secrets.db_key_base,
+ marshal: true,
+ encode: true,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ algorithm: 'aes-256-cbc'
+
+ default_value_for :only_protected_branches, true
+
+ belongs_to :project, inverse_of: :remote_mirrors
+
+ validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
+ validates :url, addressable_url: true, if: :url_changed?
+
+ before_save :set_new_remote_name, if: :mirror_url_changed?
+
+ after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
+ after_save :refresh_remote, if: :mirror_url_changed?
+ after_update :reset_fields, if: :mirror_url_changed?
+
+ after_commit :remove_remote, on: :destroy
+
+ scope :enabled, -> { where(enabled: true) }
+ scope :started, -> { with_update_status(:started) }
+ scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.day.ago, 1.day.ago) }
+
+ state_machine :update_status, initial: :none do
+ event :update_start do
+ transition [:none, :finished, :failed] => :started
+ end
+
+ event :update_finish do
+ transition started: :finished
+ end
+
+ event :update_fail do
+ transition started: :failed
+ end
+
+ state :started
+ state :finished
+ state :failed
+
+ after_transition any => :started do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path)
+
+ remote_mirror.update(last_update_started_at: Time.now)
+ end
+
+ after_transition started: :finished do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
+
+ timestamp = Time.now
+ remote_mirror.update_attributes!(
+ last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
+ )
+ end
+
+ after_transition started: :failed do |remote_mirror, _|
+ Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path)
+
+ remote_mirror.update(last_update_at: Time.now)
+ end
+ end
+
+ def remote_name
+ super || fallback_remote_name
+ end
+
+ def update_failed?
+ update_status == 'failed'
+ end
+
+ def update_in_progress?
+ update_status == 'started'
+ end
+
+ def update_repository(options)
+ raw.update(options)
+ end
+
+ def sync?
+ enabled?
+ end
+
+ def sync
+ return unless sync?
+
+ if recently_scheduled?
+ RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.now)
+ else
+ RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.now)
+ end
+ end
+
+ def enabled
+ return false unless project && super
+ return false unless project.remote_mirror_available?
+ return false unless project.repository_exists?
+ return false if project.pending_delete?
+
+ true
+ end
+ alias_method :enabled?, :enabled
+
+ def updated_since?(timestamp)
+ last_update_started_at && last_update_started_at > timestamp && !update_failed?
+ end
+
+ def mark_for_delete_if_blank_url
+ mark_for_destruction if url.blank?
+ end
+
+ def mark_as_failed(error_message)
+ update_fail
+ update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message))
+ end
+
+ def url=(value)
+ super(value) && return unless Gitlab::UrlSanitizer.valid?(value)
+
+ mirror_url = Gitlab::UrlSanitizer.new(value)
+ self.credentials = mirror_url.credentials
+
+ super(mirror_url.sanitized_url)
+ end
+
+ def url
+ if super
+ Gitlab::UrlSanitizer.new(super, credentials: credentials).full_url
+ end
+ rescue
+ super
+ end
+
+ def safe_url
+ return if url.nil?
+
+ result = URI.parse(url)
+ result.password = '*****' if result.password
+ result.user = '*****' if result.user && result.user != "git" # tokens or other data may be saved as user
+ result.to_s
+ end
+
+ private
+
+ def raw
+ @raw ||= Gitlab::Git::RemoteMirror.new(project.repository.raw, remote_name)
+ end
+
+ def fallback_remote_name
+ return unless id
+
+ "remote_mirror_#{id}"
+ end
+
+ def recently_scheduled?
+ return false unless self.last_update_started_at
+
+ self.last_update_started_at >= Time.now - backoff_delay
+ end
+
+ def backoff_delay
+ if self.only_protected_branches
+ PROTECTED_BACKOFF_DELAY
+ else
+ UNPROTECTED_BACKOFF_DELAY
+ end
+ end
+
+ def reset_fields
+ update_columns(
+ last_error: nil,
+ last_update_at: nil,
+ last_successful_update_at: nil,
+ update_status: 'finished'
+ )
+ end
+
+ def set_override_remote_mirror_available
+ enabled = read_attribute(:enabled)
+
+ project.update(remote_mirror_available_overridden: enabled)
+ end
+
+ def set_new_remote_name
+ self.remote_name = "remote_mirror_#{SecureRandom.hex}"
+ end
+
+ def refresh_remote
+ return unless project
+
+ # Before adding a new remote we have to delete the data from
+ # the previous remote name
+ prev_remote_name = remote_name_was || fallback_remote_name
+ run_after_commit do
+ project.repository.async_remove_remote(prev_remote_name)
+ end
+
+ project.repository.add_remote(remote_name, url)
+ end
+
+ def remove_remote
+ return unless project # could be pending to delete so don't need to touch the git repository
+
+ project.repository.async_remove_remote(remote_name)
+ end
+
+ def mirror_url_changed?
+ url_changed? || encrypted_credentials_changed?
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5bdaa7f0720..0e1bf11d7c0 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -37,7 +37,7 @@ class Repository
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref has_visible_content?
- issue_template_names merge_request_template_names).freeze
+ issue_template_names merge_request_template_names xcode_project?).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license).freeze
@@ -55,7 +55,8 @@ class Repository
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar,
issue_template: :issue_template_names,
- merge_request_template: :merge_request_template_names
+ merge_request_template: :merge_request_template_names,
+ xcode_config: :xcode_project?
}.freeze
def initialize(full_path, project, disk_path: nil, is_wiki: false)
@@ -84,9 +85,14 @@ class Repository
# Return absolute path to repository
def path_to_repo
- @path_to_repo ||= File.expand_path(
- File.join(repository_storage_path, disk_path + '.git')
- )
+ @path_to_repo ||=
+ begin
+ storage = Gitlab.config.repositories.storages[@project.repository_storage]
+
+ File.expand_path(
+ File.join(storage.legacy_disk_path, disk_path + '.git')
+ )
+ end
end
def inspect
@@ -589,6 +595,11 @@ class Repository
end
cache_method :gitlab_ci_yml
+ def xcode_project?
+ file_on_head(:xcode_config, :tree).present?
+ end
+ cache_method :xcode_project?
+
def head_commit
@head_commit ||= commit(self.root_ref)
end
@@ -849,13 +860,27 @@ class Repository
add_remote(remote_name, url, mirror_refmap: refmap)
fetch_remote(remote_name, forced: forced, prune: prune)
ensure
- remove_remote(remote_name) if tmp_remote_name
+ 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
+
+ job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name)
+
+ if job_id
+ Rails.logger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
+ else
+ Rails.logger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
+ end
+
+ job_id
+ end
+
def fetch_source_branch!(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
@@ -895,11 +920,21 @@ class Repository
end
end
- def file_on_head(type)
- if head = tree(:head)
- head.blobs.find do |blob|
- Gitlab::FileDetector.type_of(blob.path) == type
+ def file_on_head(type, object_type = :blob)
+ return unless head = tree(:head)
+
+ objects =
+ case object_type
+ when :blob
+ head.blobs
+ when :tree
+ head.trees
+ else
+ raise ArgumentError, "Object type #{object_type} is not supported"
end
+
+ objects.find do |object|
+ Gitlab::FileDetector.type_of(object.path) == type
end
end
@@ -915,10 +950,6 @@ class Repository
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
- def repository_storage_path
- @project.repository_storage_path
- end
-
def rebase(user, merge_request)
raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
branch_sha: merge_request.source_branch_sha,
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 6e311806be1..3da7c301d28 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -5,14 +5,14 @@ class SentNotification < ActiveRecord::Base
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User"
- validates :project, :recipient, presence: true
+ validates :recipient, presence: true
validates :reply_key, presence: true, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
validates :in_reply_to_discussion_id, format: { with: /\A\h{40}\z/, allow_nil: true }
validate :note_valid
- after_save :keep_around_commit
+ after_save :keep_around_commit, if: :for_commit?
class << self
def reply_key
diff --git a/app/models/service.rb b/app/models/service.rb
index f7e3f7590ad..831c2ea1141 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -253,7 +253,6 @@ class Service < ActiveRecord::Base
emails_on_push
external_wiki
flowdock
- gemnasium
hipchat
irker
jira
diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
index fae1b64961a..26b4b78ac64 100644
--- a/app/models/storage/hashed_project.rb
+++ b/app/models/storage/hashed_project.rb
@@ -1,7 +1,7 @@
module Storage
class HashedProject
attr_accessor :project
- delegate :gitlab_shell, :repository_storage_path, to: :project
+ delegate :gitlab_shell, :repository_storage, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze
@@ -24,7 +24,7 @@ module Storage
end
def ensure_storage_path_exists
- gitlab_shell.add_namespace(repository_storage_path, base_dir)
+ gitlab_shell.add_namespace(repository_storage, base_dir)
end
def rename_repo
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index 9d9e5e1d352..27cb388c702 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -1,7 +1,7 @@
module Storage
class LegacyProject
attr_accessor :project
- delegate :namespace, :gitlab_shell, :repository_storage_path, to: :project
+ delegate :namespace, :gitlab_shell, :repository_storage, to: :project
def initialize(project)
@project = project
@@ -24,18 +24,18 @@ module Storage
def ensure_storage_path_exists
return unless namespace
- gitlab_shell.add_namespace(repository_storage_path, base_dir)
+ gitlab_shell.add_namespace(repository_storage, base_dir)
end
def rename_repo
new_full_path = project.build_full_path
- if gitlab_shell.mv_repository(repository_storage_path, project.full_path_was, new_full_path)
+ if gitlab_shell.mv_repository(repository_storage, project.full_path_was, new_full_path)
# If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository
# So we basically we mute exceptions in next actions
begin
- gitlab_shell.mv_repository(repository_storage_path, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki")
+ gitlab_shell.mv_repository(repository_storage, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki")
return true
rescue => e
Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}"
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 29035480371..1c2161accc4 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -17,7 +17,11 @@ class SystemNoteMetadata < ActiveRecord::Base
].freeze
validates :note, presence: true
- validates :action, inclusion: ICON_TYPES, allow_nil: true
+ validates :action, inclusion: { in: :icon_types }, allow_nil: true
belongs_to :note
+
+ def icon_types
+ ICON_TYPES
+ end
end
diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb
new file mode 100644
index 00000000000..8458a231bbd
--- /dev/null
+++ b/app/models/term_agreement.rb
@@ -0,0 +1,6 @@
+class TermAgreement < ActiveRecord::Base
+ belongs_to :term, class_name: 'ApplicationSetting::Term'
+ belongs_to :user
+
+ validates :user, :term, presence: true
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index aad2c1dac4e..a2ab405fdbe 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -22,7 +22,7 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
- belongs_to :target, -> { auto_include(false) }, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index d5c5c0964c5..0a838d34054 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include IgnorableColumn
include BulkMemberAccessLoad
include BlocksJsonSerialization
+ include WithUploads
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -96,23 +97,23 @@ class User < ActiveRecord::Base
# Groups
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
- has_many :groups, -> { auto_include(false) }, through: :group_members
- has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }).auto_include(false) }, through: :group_members, source: :group
- has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }).auto_include(false) }, through: :group_members, source: :group
+ has_many :groups, through: :group_members
+ has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group
+ has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group
# Projects
- has_many :groups_projects, -> { auto_include(false) }, through: :groups, source: :projects
- has_many :personal_projects, -> { auto_include(false) }, through: :namespace, source: :projects
+ has_many :groups_projects, through: :groups, source: :projects
+ has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }
- has_many :projects, -> { auto_include(false) }, through: :project_members
- has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
+ has_many :projects, through: :project_members
+ has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :starred_projects, -> { auto_include(false) }, through: :users_star_projects, source: :project
- has_many :project_authorizations
- has_many :authorized_projects, -> { auto_include(false) }, through: :project_authorizations, source: :project
+ has_many :starred_projects, through: :users_star_projects, source: :project
+ has_many :project_authorizations, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :user_interacted_projects
- has_many :project_interactions, -> { auto_include(false) }, through: :user_interacted_projects, source: :project, class_name: 'Project'
+ has_many :project_interactions, through: :user_interacted_projects, source: :project, class_name: 'Project'
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
@@ -132,12 +133,13 @@ class User < ActiveRecord::Base
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
- has_many :assigned_issues, -> { auto_include(false) }, class_name: "Issue", through: :issue_assignees, source: :issue
+ has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
- has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :term_agreements
+ belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
#
# Validations
@@ -163,8 +165,7 @@ class User < ActiveRecord::Base
validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
- before_validation :set_notification_email, if: :email_changed?
- before_save :set_notification_email, if: :email_changed? # in case validation is skipped
+ before_validation :set_notification_email, if: :new_record?
before_validation :set_public_email, if: :public_email_changed?
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :ensure_incoming_email_token
@@ -177,8 +178,21 @@ class User < ActiveRecord::Base
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
- after_commit :update_emails_with_primary_email, on: :update, if: -> { previous_changes.key?('email') }
- after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
+ after_commit(on: :update) do
+ if previous_changes.key?('email')
+ # Grab previous_email here since previous_changes changes after
+ # #update_emails_with_primary_email and #update_notification_email are called
+ previous_email = previous_changes[:email][0]
+
+ update_emails_with_primary_email(previous_email)
+ update_invalid_gpg_signatures
+
+ if previous_email == notification_email
+ self.notification_email = email
+ save
+ end
+ end
+ end
after_initialize :set_projects_limit
@@ -235,14 +249,18 @@ class User < ActiveRecord::Base
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
- def self.with_two_factor
+ def self.with_two_factor_indistinct
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
- .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id])
+ .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true)
+ end
+
+ def self.with_two_factor
+ with_two_factor_indistinct.distinct(arel_table[:id])
end
def self.without_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
- .where("u2f.id IS NULL AND otp_required_for_login = ?", false)
+ .where("u2f.id IS NULL AND users.otp_required_for_login = ?", false)
end
#
@@ -540,8 +558,7 @@ class User < ActiveRecord::Base
# hash and `_was` variables getting munged.
# By using an `after_commit` instead of `after_update`, we avoid the recursive callback
# scenario, though it then requires us to use the `previous_changes` hash
- def update_emails_with_primary_email
- previous_email = previous_changes[:email][0] # grab this before the DestroyService is called
+ def update_emails_with_primary_email(previous_email)
primary_email_record = emails.find_by(email: email)
Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
@@ -766,13 +783,13 @@ class User < ActiveRecord::Base
end
def set_notification_email
- if notification_email.blank? || !all_emails.include?(notification_email)
+ if notification_email.blank? || all_emails.exclude?(notification_email)
self.notification_email = email
end
end
def set_public_email
- if public_email.blank? || !all_emails.include?(public_email)
+ if public_email.blank? || all_emails.exclude?(public_email)
self.public_email = ''
end
end
@@ -854,6 +871,16 @@ class User < ActiveRecord::Base
confirmed? && !temp_oauth_email?
end
+ def accept_pending_invitations!
+ pending_invitations.select do |member|
+ member.accept_invite!(self)
+ end
+ end
+
+ def pending_invitations
+ Member.where(invite_email: verified_emails).invite
+ end
+
def all_emails
all_emails = []
all_emails << email unless temp_oauth_email?
@@ -910,7 +937,7 @@ class User < ActiveRecord::Base
def delete_async(deleted_by:, params: {})
block if params[:hard_delete]
- DeleteUserWorker.perform_async(deleted_by.id, id, params)
+ DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end
def notification_service
@@ -947,10 +974,13 @@ class User < ActiveRecord::Base
end
def manageable_groups
- union = Gitlab::SQL::Union.new([owned_groups.select(:id),
- masters_groups.select(:id)])
- arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
- owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
+ union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql
+
+ # Update this line to not use raw SQL when migrated to Rails 5.2.
+ # Either ActiveRecord or Arel constructions are fine.
+ # This was replaced with the raw SQL construction because of bugs in the arel gem.
+ # Bugs were fixed in arel 9.0.0 (Rails 5.2).
+ owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
@@ -990,12 +1020,19 @@ class User < ActiveRecord::Base
!solo_owned_groups.present?
end
- def ci_authorized_runners
- @ci_authorized_runners ||= begin
- runner_ids = Ci::RunnerProject
+ def ci_owned_runners
+ @ci_owned_runners ||= begin
+ project_runner_ids = Ci::RunnerProject
.where(project: authorized_projects(Gitlab::Access::MASTER))
.select(:runner_id)
- Ci::Runner.specific.where(id: runner_ids)
+
+ group_runner_ids = Ci::RunnerNamespace
+ .where(namespace_id: owned_or_masters_groups.select(:id))
+ .select(:runner_id)
+
+ union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
+
+ Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
end
@@ -1088,8 +1125,11 @@ class User < ActiveRecord::Base
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
+ return if ::Gitlab::Database.read_only?
+
self.failed_attempts ||= 0
self.failed_attempts += 1
+
if attempts_exceeded?
lock_access! unless access_locked?
else
@@ -1184,6 +1224,20 @@ class User < ActiveRecord::Base
max_member_access_for_group_ids([group_id])[group_id]
end
+ def terms_accepted?
+ accepted_term_id.present?
+ end
+
+ def required_terms_not_accepted?
+ Gitlab::CurrentSettings.current_application_settings.enforce_terms? &&
+ !terms_accepted?
+ end
+
+ def owned_or_masters_groups
+ union = Gitlab::SQL::Union.new([owned_groups, masters_groups])
+ Group.from("(#{union.to_sql}) namespaces")
+ end
+
protected
# override, from Devise::Validatable
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index e4b69382626..9d461c6750a 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -2,7 +2,8 @@ class UserCallout < ActiveRecord::Base
belongs_to :user
enum feature_name: {
- gke_cluster_integration: 1
+ gke_cluster_integration: 1,
+ gcp_signup_offer: 2
}
validates :user, presence: true
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 0f5536415f7..cde79b95062 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -265,6 +265,15 @@ class WikiPage
title.present? && self.class.unhyphenize(@page.url_path) != title
end
+ # Updates the current @attributes hash by merging a hash of params
+ def update_attributes(attrs)
+ attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
+
+ attrs.slice!(:content, :format, :message, :title)
+
+ @attributes.merge!(attrs)
+ end
+
private
# Process and format the title based on the user input.
@@ -290,15 +299,6 @@ class WikiPage
File.join(components)
end
- # Updates the current @attributes hash by merging a hash of params
- def update_attributes(attrs)
- attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
-
- attrs.slice!(:content, :format, :message, :title)
-
- @attributes.merge!(attrs)
- end
-
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
diff --git a/app/policies/application_setting/term_policy.rb b/app/policies/application_setting/term_policy.rb
new file mode 100644
index 00000000000..f03bf748c76
--- /dev/null
+++ b/app/policies/application_setting/term_policy.rb
@@ -0,0 +1,28 @@
+class ApplicationSetting
+ class TermPolicy < BasePolicy
+ include Gitlab::Utils::StrongMemoize
+
+ condition(:current_terms, scope: :subject) do
+ Gitlab::CurrentSettings.current_application_settings.latest_terms == @subject
+ end
+
+ condition(:terms_accepted, score: 1) do
+ agreement&.accepted
+ end
+
+ rule { ~anonymous & current_terms }.policy do
+ enable :accept_terms
+ enable :decline_terms
+ end
+
+ rule { terms_accepted }.prevent :accept_terms
+
+ def agreement
+ strong_memoize(:agreement) do
+ next nil if @user.nil? || @subject.nil?
+
+ @user.term_agreements.find_by(term: @subject)
+ end
+ end
+ end
+end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 808a81cbbf9..8b65758f3e8 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -14,11 +14,20 @@ module Ci
@subject.triggered_by?(@user)
end
+ condition(:branch_allows_maintainer_push) do
+ @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ end
+
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
end
rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
+
+ rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ enable :update_build
+ enable :update_commit_status
+ end
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 6363c382ff8..540e4235299 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -4,8 +4,16 @@ module Ci
condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) }
+ condition(:branch_allows_maintainer_push) do
+ @subject.project.branch_allows_maintainer_push?(@user, @subject.ref)
+ end
+
rule { protected_ref }.prevent :update_pipeline
+ rule { can?(:public_access) & branch_allows_maintainer_push }.policy do
+ enable :update_pipeline
+ end
+
def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project)
diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb
index 7dff8470e23..895abe87d86 100644
--- a/app/policies/ci/runner_policy.rb
+++ b/app/policies/ci/runner_policy.rb
@@ -1,16 +1,19 @@
module Ci
class RunnerPolicy < BasePolicy
with_options scope: :subject, score: 0
- condition(:shared) { @subject.is_shared? }
-
- with_options scope: :subject, score: 0
condition(:locked, scope: :subject) { @subject.locked? }
- condition(:authorized_runner) { @user.ci_authorized_runners.include?(@subject) }
+ condition(:owned_runner) { @user.ci_owned_runners.exists?(@subject.id) }
rule { anonymous }.prevent_all
- rule { admin | authorized_runner }.enable :assign_runner
- rule { ~admin & shared }.prevent :assign_runner
+
+ rule { admin | owned_runner }.policy do
+ enable :assign_runner
+ enable :read_runner
+ enable :update_runner
+ enable :delete_runner
+ end
+
rule { ~admin & locked }.prevent :assign_runner
end
end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 64e550d19d0..1cf5515d9d7 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -1,22 +1,24 @@
class GlobalPolicy < BasePolicy
desc "User is blocked"
with_options scope: :user, score: 0
- condition(:blocked) { @user.blocked? }
+ condition(:blocked) { @user&.blocked? }
desc "User is an internal user"
with_options scope: :user, score: 0
- condition(:internal) { @user.internal? }
+ condition(:internal) { @user&.internal? }
desc "User's access has been locked"
with_options scope: :user, score: 0
- condition(:access_locked) { @user.access_locked? }
+ condition(:access_locked) { @user&.access_locked? }
- condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
+ condition(:can_create_fork, scope: :user) { @user && @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } }
+
+ condition(:required_terms_not_accepted, scope: :user, score: 0) do
+ @user&.required_terms_not_accepted?
+ end
rule { anonymous }.policy do
prevent :log_in
- prevent :access_api
- prevent :access_git
prevent :receive_notifications
prevent :use_quick_actions
prevent :create_group
@@ -38,6 +40,11 @@ class GlobalPolicy < BasePolicy
prevent :use_quick_actions
end
+ rule { required_terms_not_accepted }.policy do
+ prevent :access_api
+ prevent :access_git
+ end
+
rule { can_create_group }.policy do
enable :create_group
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index c9cb730c4e9..520710b757d 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do
- GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
+ GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
end
with_options scope: :subject, score: 0
@@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy
end
rule { admin } .enable :read_group
- rule { has_projects } .enable :read_group
+
+ rule { has_projects }.policy do
+ enable :read_group
+ enable :read_label
+ end
rule { has_access }.enable :read_namespace
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 3529d0aa60c..99a0d7118f2 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -76,10 +76,15 @@ class ProjectPolicy < BasePolicy
condition(:request_access_enabled, scope: :subject, score: 0) { project.request_access_enabled }
desc "Has merge requests allowing pushes to user"
- condition(:has_merge_requests_allowing_pushes, scope: :subject) do
+ condition(:has_merge_requests_allowing_pushes) do
project.merge_requests_allowing_push_to_user(user).any?
end
+ with_scope :global
+ condition(:mirror_available, score: 0) do
+ ::Gitlab::CurrentSettings.current_application_settings.mirror_available
+ end
+
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
@@ -246,6 +251,8 @@ class ProjectPolicy < BasePolicy
enable :create_cluster
end
+ rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
+
rule { archived }.policy do
prevent :push_code
prevent :push_to_delete_protected_branch
@@ -347,9 +354,7 @@ class ProjectPolicy < BasePolicy
# to run pipelines for the branches they have access to.
rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
enable :create_build
- enable :update_build
enable :create_pipeline
- enable :update_pipeline
end
rule do
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 0905ddd9b38..ee219f0a0d0 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -8,6 +8,8 @@ class UserPolicy < BasePolicy
rule { ~restricted_public_level }.enable :read_user
rule { ~anonymous }.enable :read_user
- rule { user_is_self | admin }.enable :destroy_user
- rule { subject_ghost }.prevent :destroy_user
+ rule { ~subject_ghost & (user_is_self | admin) }.policy do
+ enable :destroy_user
+ enable :update_user
+ end
end
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 9afebda19be..e0aaa5cb736 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -1,7 +1,5 @@
module Ci
- class BuildPresenter < Gitlab::View::Presenter::Delegated
- presents :build
-
+ class BuildPresenter < CommitStatusPresenter
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 099b4720fb6..cc2bce9862d 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -1,11 +1,21 @@
module Ci
class PipelinePresenter < Gitlab::View::Presenter::Delegated
+ include Gitlab::Utils::StrongMemoize
+
FAILURE_REASONS = {
config_error: 'CI/CD YAML configuration error!'
}.freeze
presents :pipeline
+ def failed_builds
+ return [] unless can?(current_user, :read_build, pipeline)
+
+ strong_memoize(:failed_builds) do
+ pipeline.builds.latest.failed
+ end
+ end
+
def failure_reason
return unless pipeline.failure_reason?
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
new file mode 100644
index 00000000000..c7f7aa836bd
--- /dev/null
+++ b/app/presenters/commit_status_presenter.rb
@@ -0,0 +1,24 @@
+class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
+ CALLOUT_FAILURE_MESSAGES = {
+ unknown_failure: 'There is an unknown failure, please try again',
+ script_failure: 'There has been a script failure. Check the job log for more information',
+ api_failure: 'There has been an API failure, please try again',
+ stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again',
+ runner_system_failure: 'There has been a runner system failure, please try again',
+ missing_dependency_failure: 'There has been a missing dependency failure, check the job log for more information'
+ }.freeze
+
+ presents :build
+
+ def callout_failure_message
+ CALLOUT_FAILURE_MESSAGES[failure_reason.to_sym]
+ end
+
+ def recoverable?
+ failed? && !unrecoverable?
+ end
+
+ def unrecoverable?
+ script_failure? || missing_dependency_failure?
+ end
+end
diff --git a/app/presenters/generic_commit_status_presenter.rb b/app/presenters/generic_commit_status_presenter.rb
new file mode 100644
index 00000000000..da09df29a37
--- /dev/null
+++ b/app/presenters/generic_commit_status_presenter.rb
@@ -0,0 +1,2 @@
+class GenericCommitStatusPresenter < CommitStatusPresenter
+end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 4b4132af2d0..ad839d9840a 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -20,6 +20,17 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
+ def unmergeable_reasons
+ strong_memoize(:unmergeable_reasons) do
+ reasons = []
+ reasons << "no commits" if merge_request.has_no_commits?
+ reasons << "source branch is missing" unless merge_request.source_branch_exists?
+ reasons << "target branch is missing" unless merge_request.target_branch_exists?
+ reasons << "has merge conflicts" unless merge_request.project.repository.can_be_merged?(merge_request.diff_head_sha, merge_request.target_branch)
+ reasons
+ end
+ end
+
def cancel_merge_when_pipeline_succeeds_path
if can_cancel_merge_when_pipeline_succeeds?(current_user)
cancel_merge_when_pipeline_succeeds_project_merge_request_path(project, merge_request)
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 484ac64580d..ad655a7b3f4 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper
include StorageHelper
include TreeHelper
+ include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
presents :project
@@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def can_current_user_push_to_branch?(branch)
- return false unless repository.branch_exists?(branch)
+ user_access(project).can_push_to_branch?(branch)
+ end
- ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
+ def can_current_user_push_to_default_branch?
+ can_current_user_push_to_branch?(default_branch)
end
def files_anchor_data
@@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def new_file_anchor_data
- if current_user && can_current_user_push_code?
+ if current_user && can_current_user_push_to_default_branch?
OpenStruct.new(enabled: false,
label: _('New file'),
link: project_new_blob_path(project, default_branch || 'master'),
@@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def readme_anchor_data
- if current_user && can_current_user_push_code? && repository.readme.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
OpenStruct.new(enabled: false,
label: _('Add Readme'),
link: add_readme_path)
@@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def changelog_anchor_data
- if current_user && can_current_user_push_code? && repository.changelog.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
OpenStruct.new(enabled: false,
label: _('Add Changelog'),
link: add_changelog_path)
@@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def license_anchor_data
- if current_user && can_current_user_push_code? && repository.license_blob.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
OpenStruct.new(enabled: false,
label: _('Add License'),
link: add_license_path)
@@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def contribution_guide_anchor_data
- if current_user && can_current_user_push_code? && repository.contribution_guide.blank?
+ if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
OpenStruct.new(enabled: false,
label: _('Add Contribution guide'),
link: add_contribution_guide_path)
@@ -260,7 +263,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
OpenStruct.new(enabled: auto_devops_enabled?,
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
- link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled?
OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'),
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
index 71d9a65fb58..464217123b4 100644
--- a/app/serializers/entity_date_helper.rb
+++ b/app/serializers/entity_date_helper.rb
@@ -1,5 +1,6 @@
module EntityDateHelper
include ActionView::Helpers::DateHelper
+ include ActionView::Helpers::TagHelper
def interval_in_words(diff)
return 'Not started' unless diff
@@ -34,4 +35,30 @@ module EntityDateHelper
duration_hash
end
+
+ # Generates an HTML-formatted string for remaining dates based on start_date and due_date
+ #
+ # It returns "Past due" for expired entities
+ # It returns "Upcoming" for upcoming entities
+ # If due date is provided, it returns "# days|weeks|months remaining|ago"
+ # If start date is provided and elapsed, with no due date, it returns "# days elapsed"
+ def remaining_days_in_words(entity)
+ if entity.try(:expired?)
+ content_tag(:strong, 'Past due')
+ elsif entity.try(:upcoming?)
+ content_tag(:strong, 'Upcoming')
+ elsif entity.due_date
+ is_upcoming = (entity.due_date - Date.today).to_i > 0
+ time_ago = time_ago_in_words(entity.due_date)
+ content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
+ content.slice!("about ")
+ content << " " + (is_upcoming ? _("remaining") : _("ago"))
+ content.html_safe
+ elsif entity.start_date && entity.start_date.past?
+ days = entity.elapsed_days
+ content = content_tag(:strong, days)
+ content << " #{'day'.pluralize(days)} elapsed"
+ content.html_safe
+ end
+ end
end
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index 523b522d449..3076fed1674 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -26,6 +26,8 @@ class JobEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
+ expose :callout_message, if: -> (*) { failed? }
+ expose :recoverable, if: -> (*) { failed? }
private
@@ -50,4 +52,20 @@ class JobEntity < Grape::Entity
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
end
+
+ def failed?
+ build.failed?
+ end
+
+ def callout_message
+ build_presenter.callout_failure_message
+ end
+
+ def recoverable
+ build_presenter.recoverable?
+ end
+
+ def build_presenter
+ @build_presenter ||= build.present
+ end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 4a812e39ee1..d0165c148eb 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -2,6 +2,7 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :state
expose :in_progress_merge_commit_sha
expose :merge_commit_sha
+ expose :short_merge_commit_sha
expose :merge_error
expose :merge_params
expose :merge_status
@@ -207,6 +208,12 @@ class MergeRequestWidgetEntity < IssuableEntity
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
+ expose :merge_commit_path do |merge_request|
+ if merge_request.merge_commit_sha
+ project_commit_path(merge_request.project, merge_request.merge_commit_sha)
+ end
+ end
+
private
delegate :current_user, to: :request
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 6457294b285..f782b411b84 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -4,7 +4,11 @@ class PipelineEntity < Grape::Entity
expose :id
expose :user, using: UserEntity
expose :active?, as: :active
- expose :coverage
+
+ # Coverage isn't always necessary (e.g. when displaying project pipelines in
+ # the UI). Instead of creating an entirely different entity we just allow the
+ # disabling of this specific field whenever necessary.
+ expose :coverage, unless: proc { options[:disable_coverage] }
expose :source
expose :created_at, :updated_at
diff --git a/app/serializers/project_mirror_entity.rb b/app/serializers/project_mirror_entity.rb
new file mode 100644
index 00000000000..a9c08ac021a
--- /dev/null
+++ b/app/serializers/project_mirror_entity.rb
@@ -0,0 +1,11 @@
+class ProjectMirrorEntity < Grape::Entity
+ expose :id
+
+ expose :remote_mirrors_attributes do |project|
+ next [] unless project.remote_mirrors.present?
+
+ project.remote_mirrors.map do |remote|
+ remote.as_json(only: %i[id url enabled])
+ end
+ end
+end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
index 4523b15152e..2516df70ad9 100644
--- a/app/serializers/stage_entity.rb
+++ b/app/serializers/stage_entity.rb
@@ -11,6 +11,12 @@ class StageEntity < Grape::Entity
if: -> (_, opts) { opts[:grouped] },
with: JobGroupEntity
+ expose :latest_statuses,
+ if: -> (_, opts) { opts[:details] },
+ with: JobEntity do |stage|
+ latest_statuses
+ end
+
expose :detailed_status, as: :status, with: StatusEntity
expose :path do |stage|
@@ -35,4 +41,14 @@ class StageEntity < Grape::Entity
def detailed_status
stage.detailed_status(request.current_user)
end
+
+ def grouped_statuses
+ @grouped_statuses ||= stage.statuses.latest_ordered.group_by(&:status)
+ end
+
+ def latest_statuses
+ HasStatus::ORDERED_STATUSES.map do |ordered_status|
+ grouped_statuses.fetch(ordered_status, [])
+ end.flatten
+ end
end
diff --git a/app/serializers/stage_serializer.rb b/app/serializers/stage_serializer.rb
new file mode 100644
index 00000000000..091d8e91e43
--- /dev/null
+++ b/app/serializers/stage_serializer.rb
@@ -0,0 +1,7 @@
+class StageSerializer < BaseSerializer
+ include WithPagination
+
+ InvalidResourceError = Class.new(StandardError)
+
+ entity StageEntity
+end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 61589a07250..d6d3a661dab 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -1,7 +1,22 @@
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
def execute
+ update_terms(@params.delete(:terms))
+
@application_setting.update(@params)
end
+
+ private
+
+ def update_terms(terms)
+ return unless terms.present?
+
+ # Avoid creating a new terms record if the text is exactly the same.
+ terms = terms.strip
+ return if terms == @application_setting.terms
+
+ ApplicationSetting::Term.create(terms: terms)
+ @application_setting.reset_memoized_terms
+ end
end
end
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
index 7c4a79f555e..3025029755c 100644
--- a/app/services/boards/issues/create_service.rb
+++ b/app/services/boards/issues/create_service.rb
@@ -10,11 +10,15 @@ module Boards
end
def execute
- create_issue(params.merge(label_ids: [list.label_id]))
+ create_issue(params.merge(issue_params))
end
private
+ def issue_params
+ { label_ids: [list.label_id] }
+ end
+
def board
@board ||= parent.boards.find(params.delete(:board_id))
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 6ce86983287..17a53b6a8fd 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -24,6 +24,7 @@ module Ci
ignore_skip_ci: ignore_skip_ci,
save_incompleted: save_on_errors,
seeds_block: block,
+ variables_attributes: params[:variables_attributes],
project: project,
current_user: current_user)
diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb
index 87f19b333de..b8c7be2d350 100644
--- a/app/services/ci/ensure_stage_service.rb
+++ b/app/services/ci/ensure_stage_service.rb
@@ -42,6 +42,7 @@ module Ci
def create_stage
Ci::Stage.create!(name: @build.stage,
+ position: @build.stage_idx,
pipeline: @build.pipeline,
project: @build.project)
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index c289b44f885..4291631913a 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -17,8 +17,10 @@ module Ci
builds =
if runner.shared?
builds_for_shared_runner
+ elsif runner.group_type?
+ builds_for_group_runner
else
- builds_for_specific_runner
+ builds_for_project_runner
end
valid = true
@@ -33,7 +35,7 @@ module Ci
end
end
- builds.auto_include(false).find do |build|
+ builds.find do |build|
next unless runner.can_pick?(build)
begin
@@ -75,15 +77,24 @@ module Ci
.joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id')
.where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
- # Implement fair scheduling
- # this returns builds that are ordered by number of running builds
- # we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
+ # Implement fair scheduling
+ # this returns builds that are ordered by number of running builds
+ # we prefer projects that don't use shared runners at all
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
.order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
- def builds_for_specific_runner
- new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC')
+ def builds_for_project_runner
+ new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC')
+ end
+
+ def builds_for_group_runner
+ hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants
+ projects = Project.where(namespace_id: hierarchy_groups)
+ .with_group_runners_enabled
+ .with_builds_enabled
+ .without_deleted
+ new_builds.where(project: projects).order('id ASC')
end
def running_builds_for_shared_runners
@@ -97,10 +108,6 @@ module Ci
builds
end
- def shared_runner_build_limits_feature_enabled?
- ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
- end
-
def register_failure
failed_attempt_counter.increment
attempt_counter.increment
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
index 152c8ae5006..41b1c144c3e 100644
--- a/app/services/ci/update_build_queue_service.rb
+++ b/app/services/ci/update_build_queue_service.rb
@@ -1,18 +1,14 @@
module Ci
class UpdateBuildQueueService
def execute(build)
- build.project.runners.each do |runner|
- if runner.can_pick?(build)
- runner.tick_runner_queue
- end
- end
+ tick_for(build, build.project.all_runners)
+ end
- return unless build.project.shared_runners_enabled?
+ private
- Ci::Runner.shared.each do |runner|
- if runner.can_pick?(build)
- runner.tick_runner_queue
- end
+ def tick_for(build, runners)
+ runners.each do |runner|
+ runner.pick_build!(build)
end
end
end
diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb
index eb8caa68ef7..9c5461e85e1 100644
--- a/app/services/clusters/applications/schedule_installation_service.rb
+++ b/app/services/clusters/applications/schedule_installation_service.rb
@@ -1,21 +1,10 @@
module Clusters
module Applications
class ScheduleInstallationService < ::BaseService
- def execute
- application_class.find_or_create_by!(cluster: cluster).try do |application|
- application.make_scheduled!
- ClusterInstallAppWorker.perform_async(application.name, application.id)
- end
- end
-
- private
-
- def application_class
- params[:application_class]
- end
+ def execute(application)
+ application.make_scheduled!
- def cluster
- params[:cluster]
+ ClusterInstallAppWorker.perform_async(application.name, application.id)
end
end
end
diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb
new file mode 100644
index 00000000000..30be6accc32
--- /dev/null
+++ b/app/services/concerns/exclusive_lease_guard.rb
@@ -0,0 +1,52 @@
+#
+# Concern that helps with getting an exclusive lease for running a block
+# of code.
+#
+# `#try_obtain_lease` takes a block which will be run if it was able to
+# obtain the lease. Implement `#lease_timeout` to configure the timeout
+# for the exclusive lease. Optionally override `#lease_key` to set the
+# lease key, it defaults to the class name with underscores.
+#
+module ExclusiveLeaseGuard
+ extend ActiveSupport::Concern
+
+ def try_obtain_lease
+ lease = exclusive_lease.try_obtain
+
+ unless lease
+ log_error('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+ return
+ end
+
+ begin
+ yield lease
+ ensure
+ release_lease(lease)
+ end
+ end
+
+ def exclusive_lease
+ @lease ||= Gitlab::ExclusiveLease.new(lease_key, timeout: lease_timeout)
+ end
+
+ def lease_key
+ @lease_key ||= self.class.name.underscore
+ end
+
+ def lease_timeout
+ raise NotImplementedError,
+ "#{self.class.name} does not implement #{__method__}"
+ end
+
+ def release_lease(uuid)
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+
+ def renew_lease!
+ exclusive_lease.renew
+ end
+
+ def log_error(message, extra_args = {})
+ logger.error(message)
+ end
+end
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
new file mode 100644
index 00000000000..bf60b96938d
--- /dev/null
+++ b/app/services/concerns/users/participable_service.rb
@@ -0,0 +1,41 @@
+module Users
+ module ParticipableService
+ extend ActiveSupport::Concern
+
+ included do
+ attr_reader :noteable
+ end
+
+ def noteable_owner
+ return [] unless noteable && noteable.author.present?
+
+ [as_hash(noteable.author)]
+ end
+
+ def participants_in_noteable
+ return [] unless noteable
+
+ users = noteable.participants(current_user)
+ sorted(users)
+ end
+
+ def sorted(users)
+ users.uniq.to_a.compact.sort_by(&:username).map do |user|
+ as_hash(user)
+ end
+ end
+
+ def groups
+ current_user.authorized_groups.sort_by(&:path).map do |group|
+ count = group.users.count
+ { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
+ end
+ end
+
+ private
+
+ def as_hash(user)
+ { username: user.username, name: user.name, avatar_url: user.avatar_url }
+ end
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index c037141fcde..f3bfc53dcd3 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -55,6 +55,7 @@ class GitPushService < BaseService
execute_related_hooks
perform_housekeeping
+ update_remote_mirrors
update_caches
update_signatures
@@ -119,6 +120,13 @@ class GitPushService < BaseService
protected
+ def update_remote_mirrors
+ return unless @project.has_remote_mirror?
+
+ @project.mark_stuck_remote_mirrors_as_failed!
+ @project.update_remote_mirrors
+ end
+
def execute_related_hooks
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index fee5bc38f7b..4a99367c575 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -26,7 +26,7 @@ module Issues
issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
- notification_service.close_issue(issue, current_user) if notifications
+ notification_service.async.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 7140890d201..78e79344c99 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -139,7 +139,7 @@ module Issues
end
def notify_participants
- notification_service.issue_moved(@old_issue, @new_issue, @current_user)
+ notification_service.async.issue_moved(@old_issue, @new_issue, @current_user)
end
end
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 62b4b4b6a1e..02224f3357a 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -6,7 +6,7 @@ module Issues
if issue.reopen
event_service.reopen_issue(issue, current_user)
create_note(issue, 'reopened')
- notification_service.reopen_issue(issue, current_user)
+ notification_service.async.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 1374f10c586..1000e1842b6 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -30,7 +30,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
- notification_service.reassigned_issue(issue, current_user, old_assignees)
+ notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user, old_assignees)
end
@@ -41,13 +41,13 @@ module Issues
added_labels = issue.labels - old_labels
if added_labels.present?
- notification_service.relabeled_issue(issue, added_labels, current_user)
+ notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
added_mentions = issue.mentioned_users - old_mentioned_users
if added_mentions.present?
- notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
+ notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
end
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
index f78791932a7..df8e82f5f60 100644
--- a/app/services/keys/base_service.rb
+++ b/app/services/keys/base_service.rb
@@ -2,7 +2,7 @@ module Keys
class BaseService
attr_accessor :user, :params
- def initialize(user, params)
+ def initialize(user, params = {})
@user, @params = user, params
@ip_address = @params.delete(:ip_address)
end
diff --git a/app/services/keys/destroy_service.rb b/app/services/keys/destroy_service.rb
new file mode 100644
index 00000000000..785cfa3a1d8
--- /dev/null
+++ b/app/services/keys/destroy_service.rb
@@ -0,0 +1,12 @@
+module Keys
+ class DestroyService < ::Keys::BaseService
+ def execute(key)
+ key.destroy if destroy_possible?(key)
+ end
+
+ # overriden in EE::Keys::DestroyService
+ def destroy_possible?(key)
+ true
+ end
+ end
+end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index 775efed48eb..9b7486cf53b 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -64,9 +64,14 @@ module Labels
end
def update_label_links(labels, old_label_id:, new_label_id:)
- LabelLink.joins(:label)
- .merge(labels)
- .where(label_id: old_label_id)
+ # use 'labels' relation to get label_link ids only of issues/MRs
+ # in the project being transferred.
+ # IDs are fetched in a separate query because MySQL doesn't
+ # allow referring of 'label_links' table in UPDATE query:
+ # https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/62435068
+ link_ids = labels.pluck('label_links.id')
+
+ LabelLink.where(id: link_ids, label_id: old_label_id)
.update_all(label_id: new_label_id)
end
diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb
index 6c93dc69bb0..7eb89339a92 100644
--- a/app/services/lfs/unlock_file_service.rb
+++ b/app/services/lfs/unlock_file_service.rb
@@ -2,14 +2,14 @@ module Lfs
class UnlockFileService < BaseService
def execute
unless can?(current_user, :push_code, project)
- raise Gitlab::GitAccess::UnauthorizedError, 'You have no permissions'
+ raise Gitlab::GitAccess::UnauthorizedError, _('You have no permissions')
end
unlock_file
rescue Gitlab::GitAccess::UnauthorizedError => ex
error(ex.message, 403)
rescue ActiveRecord::RecordNotFound
- error('Lock not found', 404)
+ error(_('Lock not found'), 404)
rescue => ex
error(ex.message, 500)
end
@@ -24,9 +24,9 @@ module Lfs
success(lock: lock, http_status: :ok)
elsif forced
- error('You must have master access to force delete a lock', 403)
+ error(_('You must have master access to force delete a lock'), 403)
else
- error("#{lock.path} is locked by GitLab User #{lock.user_id}", 403)
+ error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403)
end
end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index f727ec002e7..db701c1145d 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -10,7 +10,7 @@ module MergeRequests
if merge_request.close
create_event(merge_request)
create_note(merge_request)
- notification_service.close_mr(merge_request, current_user)
+ notification_service.async.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index cedfcb50e09..2209a60a840 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -50,21 +50,30 @@ module MergeRequests
end
def commit
- message = params[:commit_message] || merge_request.merge_commit_message
-
log_info("Git merge started on JID #{merge_jid}")
- commit_id = repository.merge(current_user, source, merge_request, message)
- log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
+ commit_id = try_merge
+
+ if commit_id
+ log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
+ else
+ raise MergeError, 'Conflicts detected during merge'
+ end
- raise MergeError, 'Conflicts detected during merge' unless commit_id
+ merge_request.update!(merge_commit_sha: commit_id)
+ end
+
+ def try_merge
+ message = params[:commit_message] || merge_request.merge_commit_message
- merge_request.update(merge_commit_sha: commit_id)
+ repository.merge(current_user, source, merge_request, message)
rescue Gitlab::Git::HooksService::PreReceiveError => e
- raise MergeError, e.message
- rescue StandardError => e
- raise MergeError, "Something went wrong during merge: #{e.message}"
+ handle_merge_error(log_message: e.message)
+ raise MergeError, 'Something went wrong during merge pre-receive hook'
+ rescue => e
+ handle_merge_error(log_message: e.message)
+ raise MergeError, 'Something went wrong during merge'
ensure
- merge_request.update(in_progress_merge_commit_sha: nil)
+ merge_request.update!(in_progress_merge_commit_sha: nil)
end
def after_merge
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
index 850deb0ac7a..9a4e6eb2e88 100644
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -24,11 +24,7 @@ module MergeRequests
pipeline_merge_requests(pipeline) do |merge_request|
next unless merge_request.merge_when_pipeline_succeeds?
-
- unless merge_request.mergeable?
- todo_service.merge_request_became_unmergeable(merge_request)
- next
- end
+ next unless merge_request.mergeable?
merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 1fb1796b56c..0127d781686 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -3,6 +3,12 @@ module MergeRequests
def execute(oldrev, newrev, ref)
return true unless Gitlab::Git.branch_ref?(ref)
+ do_execute(oldrev, newrev, ref)
+ end
+
+ private
+
+ def do_execute(oldrev, newrev, ref)
@oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref)
@@ -28,8 +34,6 @@ module MergeRequests
true
end
- private
-
def close_upon_missing_source_branch_ref
# MergeRequest#reload_diff ignores not opened MRs. This means it won't
# create an `empty` diff for `closed` MRs without a source branch, keeping
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index 120677a7149..8f1c95ac1b7 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
if merge_request.reopen
create_event(merge_request)
create_note(merge_request, 'reopened')
- notification_service.reopen_mr(merge_request, current_user)
+ notification_service.async.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
diff --git a/app/services/merge_requests/resolved_discussion_notification_service.rb b/app/services/merge_requests/resolved_discussion_notification_service.rb
index 3a09350c847..66a0cbc81d4 100644
--- a/app/services/merge_requests/resolved_discussion_notification_service.rb
+++ b/app/services/merge_requests/resolved_discussion_notification_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
return unless merge_request.discussions_resolved?
SystemNoteService.resolve_all_discussions(merge_request, project, current_user)
- notification_service.resolve_all_discussions(merge_request, current_user)
+ notification_service.async.resolve_all_discussions(merge_request, current_user)
end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 8a40ad88182..7350725e223 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
update(merge_request)
end
+ # rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
@@ -42,8 +43,11 @@ module MergeRequests
end
if merge_request.previous_changes.include?('assignee_id')
+ old_assignee_id = merge_request.previous_changes['assignee_id'].first
+ old_assignee = User.find(old_assignee_id) if old_assignee_id
+
create_assignee_note(merge_request)
- notification_service.reassigned_merge_request(merge_request, current_user)
+ notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignee)
todo_service.reassigned_merge_request(merge_request, current_user)
end
@@ -54,7 +58,7 @@ module MergeRequests
added_labels = merge_request.labels - old_labels
if added_labels.present?
- notification_service.relabeled_merge_request(
+ notification_service.async.relabeled_merge_request(
merge_request,
added_labels,
current_user
@@ -63,13 +67,14 @@ module MergeRequests
added_mentions = merge_request.mentioned_users - old_mentioned_users
if added_mentions.present?
- notification_service.new_mentions_in_merge_request(
+ notification_service.async.new_mentions_in_merge_request(
merge_request,
added_mentions,
current_user
)
end
end
+ # rubocop:enable Metrics/AbcSize
def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge)
diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb
index 4963601ea8b..cce0863d611 100644
--- a/app/services/milestones/base_service.rb
+++ b/app/services/milestones/base_service.rb
@@ -5,6 +5,7 @@ module Milestones
def initialize(parent, user, params = {})
@parent, @current_user, @params = parent, user, params.dup
+ super
end
end
end
diff --git a/app/services/notes/resolve_service.rb b/app/services/notes/resolve_service.rb
new file mode 100644
index 00000000000..0db8ee809a9
--- /dev/null
+++ b/app/services/notes/resolve_service.rb
@@ -0,0 +1,9 @@
+module Notes
+ class ResolveService < ::BaseService
+ def execute(note)
+ note.resolve!(current_user)
+
+ ::MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
+ end
+ end
+end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 83e59a649b6..4fa38665abc 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -18,6 +18,10 @@ module NotificationRecipientService
Builder::NewNote.new(*a).notification_recipients
end
+ def self.build_merge_request_unmergeable_recipients(*a)
+ Builder::MergeRequestUnmergeable.new(*a).notification_recipients
+ end
+
module Builder
class Base
def initialize(*)
@@ -45,6 +49,10 @@ module NotificationRecipientService
target.project
end
+ def group
+ project&.group || target.try(:group)
+ end
+
def recipients
@recipients ||= []
end
@@ -67,6 +75,7 @@ module NotificationRecipientService
user, type,
reason: reason,
project: project,
+ group: group,
custom_action: custom_action,
target: target,
acting_user: acting_user
@@ -107,11 +116,11 @@ module NotificationRecipientService
# Users with a notification setting on group or project
user_ids += user_ids_notifiable_on(project, :custom)
- user_ids += user_ids_notifiable_on(project.group, :custom)
+ user_ids += user_ids_notifiable_on(group, :custom)
# Users with global level custom
user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
- user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(group, :global)
global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action)
@@ -123,6 +132,10 @@ module NotificationRecipientService
add_recipients(project_watchers, :watch, nil)
end
+ def add_group_watchers
+ add_recipients(group_watchers, :watch, nil)
+ end
+
# Get project users with WATCH notification level
def project_watchers
project_members_ids = user_ids_notifiable_on(project)
@@ -138,6 +151,14 @@ module NotificationRecipientService
user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq)
end
+ def group_watchers
+ user_ids_with_group_global = user_ids_notifiable_on(group, :global)
+ user_ids = user_ids_with_global_level_watch(user_ids_with_group_global)
+ user_ids_with_group_setting = select_group_members_ids(group, [], user_ids_with_group_global, user_ids)
+
+ user_scope.where(id: user_ids_with_group_setting)
+ end
+
def add_subscribed_users
return unless target.respond_to? :subscribers
@@ -281,6 +302,14 @@ module NotificationRecipientService
note.project
end
+ def group
+ if note.for_project_noteable?
+ project.group
+ else
+ target.try(:group)
+ end
+ end
+
def build!
# Add all users participating in the thread (author, assignee, comment authors)
add_participants(note.author)
@@ -289,11 +318,11 @@ module NotificationRecipientService
if note.for_project_noteable?
# Merge project watchers
add_project_watchers
-
- # Merge project with custom notification
- add_custom_notifications
+ else
+ add_group_watchers
end
+ add_custom_notifications
add_subscribed_users
end
@@ -305,5 +334,26 @@ module NotificationRecipientService
note.author
end
end
+
+ class MergeRequestUnmergeable < Base
+ attr_reader :target
+ def initialize(merge_request)
+ @target = merge_request
+ end
+
+ def build!
+ target.merge_participants.each do |user|
+ add_recipients(user, :participating, nil)
+ end
+ end
+
+ def custom_action
+ :unmergeable_merge_request
+ end
+
+ def acting_user
+ nil
+ end
+ end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 274161df946..636cfbf5b45 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -7,7 +7,32 @@
# Ex.
# NotificationService.new.new_issue(issue, current_user)
#
+# When calculating the recipients of a notification is expensive (for instance,
+# in the new issue case), `#async` will make that calculation happen in Sidekiq
+# instead:
+#
+# NotificationService.new.async.new_issue(issue, current_user)
+#
class NotificationService
+ class Async
+ attr_reader :parent
+ delegate :respond_to_missing, to: :parent
+
+ def initialize(parent)
+ @parent = parent
+ end
+
+ def method_missing(meth, *args)
+ return super unless parent.respond_to?(meth)
+
+ MailScheduler::NotificationServiceWorker.perform_async(meth.to_s, *args)
+ end
+ end
+
+ def async
+ @async ||= Async.new(self)
+ end
+
# Always notify user about ssh key added
# only if ssh key is not deploy key
#
@@ -104,6 +129,7 @@ class NotificationService
# When create a merge request we should send an email to:
#
+ # * mr author
# * mr assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
@@ -123,6 +149,15 @@ class NotificationService
end
end
+ # When a merge request is found to be unmergeable, we should send an email to:
+ #
+ # * mr author
+ # * mr merge user if set
+ #
+ def merge_request_unmergeable(merge_request)
+ merge_request_unmergeable_email(merge_request)
+ end
+
# When merge request text is updated, we should send an email to:
#
# * newly mentioned project team members with notification level higher than Participating
@@ -142,8 +177,23 @@ class NotificationService
# * merge_request assignee if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
- def reassigned_merge_request(merge_request, current_user)
- reassign_resource_email(merge_request, current_user, :reassigned_merge_request_email)
+ def reassigned_merge_request(merge_request, current_user, previous_assignee)
+ recipients = NotificationRecipientService.build_recipients(
+ merge_request,
+ current_user,
+ action: "reassign",
+ previous_assignee: previous_assignee
+ )
+
+ recipients.each do |recipient|
+ mailer.reassigned_merge_request_email(
+ recipient.user.id,
+ merge_request.id,
+ previous_assignee&.id,
+ current_user.id,
+ recipient.reason
+ ).deliver_later
+ end
end
# When we add labels to a merge request we should send an email to:
@@ -421,29 +471,6 @@ class NotificationService
end
end
- def reassign_resource_email(target, current_user, method)
- previous_assignee_id = previous_record(target, 'assignee_id')
- previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
-
- recipients = NotificationRecipientService.build_recipients(
- target,
- current_user,
- action: "reassign",
- previous_assignee: previous_assignee
- )
-
- recipients.each do |recipient|
- mailer.send(
- method,
- recipient.user.id,
- target.id,
- previous_assignee_id,
- current_user.id,
- recipient.reason
- ).deliver_later
- end
- end
-
def relabeled_resource_email(target, labels, current_user, method)
recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
recipients = notifiable_users(
@@ -467,18 +494,18 @@ class NotificationService
end
end
- def mailer
- Notify
- end
+ def merge_request_unmergeable_email(merge_request)
+ recipients = NotificationRecipientService.build_merge_request_unmergeable_recipients(merge_request)
- def previous_record(object, attribute)
- return unless object && attribute
-
- if object.previous_changes.include?(attribute)
- object.previous_changes[attribute].first
+ recipients.each do |recipient|
+ mailer.merge_request_unmergeable_email(recipient.user.id, merge_request.id).deliver_later
end
end
+ def mailer
+ Notify
+ end
+
private
def recipients_for_pages_domain(domain)
diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb
index a549cfbabea..29b133cc466 100644
--- a/app/services/projects/create_from_template_service.rb
+++ b/app/services/projects/create_from_template_service.rb
@@ -8,9 +8,10 @@ module Projects
template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file
+ override_params = params.dup
params[:file] = file
- GitlabProjectsImportService.new(current_user, params).execute
+ GitlabProjectsImportService.new(current_user, params, override_params).execute
ensure
file&.close
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index d361d070993..d16ecdb7b9b 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -142,7 +142,7 @@ module Projects
if @project
@project.errors.add(:base, message)
- @project.mark_import_as_failed(message) if @project.import?
+ @project.mark_import_as_failed(message) if @project.persisted? && @project.import?
end
@project
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 44e869851ca..077d27c5836 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -51,11 +51,11 @@ module Projects
flush_caches(@project)
- unless mv_repository(removal_path(repo_path), repo_path)
+ unless rollback_repository(removal_path(repo_path), repo_path)
raise_error('Failed to restore project repository. Please contact the administrator.')
end
- unless mv_repository(removal_path(wiki_path), wiki_path)
+ unless rollback_repository(removal_path(wiki_path), wiki_path)
raise_error('Failed to restore wiki repository. Please contact the administrator.')
end
end
@@ -84,25 +84,38 @@ module Projects
# Skip repository removal. We use this flag when remove user or group
return true if params[:skip_repo] == true
+ # There is a possibility project does not have repository or wiki
+ return true unless repo_exists?(path)
+
new_path = removal_path(path)
if mv_repository(path, new_path)
- log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
+ log_info(%Q{Repository "#{path}" moved to "#{new_path}" for project "#{project.full_path}"})
project.run_after_commit do
# self is now project
- GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage_path, new_path)
+ GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage, new_path)
end
else
false
end
end
- def mv_repository(from_path, to_path)
+ def rollback_repository(old_path, new_path)
# There is a possibility project does not have repository or wiki
- return true unless gitlab_shell.exists?(project.repository_storage_path, from_path + '.git')
+ return true unless repo_exists?(old_path)
+
+ mv_repository(old_path, new_path)
+ end
+
+ def repo_exists?(path)
+ gitlab_shell.exists?(project.repository_storage, path + '.git')
+ end
+
+ def mv_repository(from_path, to_path)
+ return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git')
- gitlab_shell.mv_repository(project.repository_storage_path, from_path, to_path)
+ gitlab_shell.mv_repository(project.repository_storage, from_path, to_path)
end
def attempt_rollback(project, message)
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index 67178de75de..68c1af2396b 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -47,8 +47,8 @@ module Projects
private
def move_repository(from_name, to_name)
- from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
- to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
+ from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
+ to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
# If we don't find the repository on either original or target we should log that as it could be an issue if the
# project was not originally empty.
@@ -60,7 +60,7 @@ module Projects
return true
end
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
def rollback_folder_move
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
index a975a06a05c..0a004677417 100644
--- a/app/services/projects/open_issues_count_service.rb
+++ b/app/services/projects/open_issues_count_service.rb
@@ -2,14 +2,42 @@ module Projects
# Service class for counting and caching the number of open issues of a
# project.
class OpenIssuesCountService < Projects::CountService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(project, user = nil)
+ @user = user
+
+ super(project)
+ end
+
def cache_key_name
- 'open_issues_count'
+ public_only? ? 'public_open_issues_count' : 'total_open_issues_count'
+ end
+
+ def public_only?
+ !user_is_at_least_reporter?
+ end
+
+ def relation_for_count
+ self.class.query(@project, public_only: public_only?)
+ end
+
+ def user_is_at_least_reporter?
+ strong_memoize(:user_is_at_least_reporter) do
+ @user && @project.team.member?(@user, Gitlab::Access::REPORTER)
+ end
end
- def self.query(project_ids)
- # We don't include confidential issues in this number since this would
- # expose the number of confidential issues to non project members.
- Issue.opened.public_only.where(project: project_ids)
+ # We only show total issues count for reporters
+ # which are allowed to view confidential issues
+ # This will still show a discrepancy on issues number but should be less than before.
+ # Check https://gitlab.com/gitlab-org/gitlab-ce/issues/38418 description.
+ def self.query(projects, public_only: true)
+ if public_only
+ Issue.opened.public_only.where(project: projects)
+ else
+ Issue.opened.where(project: projects)
+ end
end
end
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index e6193fcacee..eb0472c6024 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,6 +1,6 @@
module Projects
class ParticipantsService < BaseService
- attr_reader :noteable
+ include Users::ParticipableService
def execute(noteable)
@noteable = noteable
@@ -10,36 +10,6 @@ module Projects
participants.uniq
end
- def noteable_owner
- return [] unless noteable && noteable.author.present?
-
- [{
- name: noteable.author.name,
- username: noteable.author.username,
- avatar_url: noteable.author.avatar_url
- }]
- end
-
- def participants_in_noteable
- return [] unless noteable
-
- users = noteable.participants(current_user)
- sorted(users)
- end
-
- def sorted(users)
- users.uniq.to_a.compact.sort_by(&:username).map do |user|
- { username: user.username, name: user.name, avatar_url: user.avatar_url }
- end
- end
-
- def groups
- current_user.authorized_groups.sort_by(&:path).map do |group|
- count = group.users.count
- { username: group.full_path, name: group.full_name, count: count, avatar_url: group.avatar_url }
- end
- end
-
def all_members
count = project.team.members.flatten.count
[{ username: "all", name: "All Project and Group Members", count: count }]
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 5a23f0f0a62..61acdd58021 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -127,7 +127,7 @@ module Projects
end
def move_repo_folder(from_name, to_name)
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
def execute_system_hooks
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index de77f6bf585..1d8caec9c6f 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -1,6 +1,6 @@
module Projects
class UpdatePagesService < BaseService
- InvaildStateError = Class.new(StandardError)
+ InvalidStateError = Class.new(StandardError)
FailedToExtractError = Class.new(StandardError)
BLOCK_SIZE = 32.kilobytes
@@ -21,8 +21,8 @@ module Projects
@status.enqueue!
@status.run!
- raise InvaildStateError, 'missing pages artifacts' unless build.artifacts?
- raise InvaildStateError, 'pages are outdated' unless latest?
+ raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
+ raise InvalidStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
FileUtils.mkdir_p(tmp_path)
@@ -31,16 +31,16 @@ module Projects
# Check if we did extract public directory
archive_public_path = File.join(archive_path, 'public')
- raise InvaildStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
- raise InvaildStateError, 'pages are outdated' unless latest?
+ raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise InvalidStateError, 'pages are outdated' unless latest?
deploy_page!(archive_public_path)
success
end
- rescue InvaildStateError => e
+ rescue InvalidStateError => e
error(e.message)
rescue => e
- error(e.message, false)
+ error(e.message)
raise e
end
@@ -48,17 +48,15 @@ module Projects
def success
@status.success
- delete_artifact!
super
end
- def error(message, allow_delete_artifact = true)
+ def error(message)
register_failure
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
@status.drop(:script_failure)
- delete_artifact! if allow_delete_artifact
super
end
@@ -77,18 +75,18 @@ module Projects
if artifacts.ends_with?('.zip')
extract_zip_archive!(temp_path)
else
- raise InvaildStateError, 'unsupported artifacts format'
+ raise InvalidStateError, 'unsupported artifacts format'
end
end
def extract_zip_archive!(temp_path)
- raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
+ raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
if public_entry.total_size > max_size
- raise InvaildStateError, "artifacts for pages are too large: #{public_entry.total_size}"
+ raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
end
# Requires UnZip at least 6.00 Info-ZIP.
@@ -162,11 +160,6 @@ module Projects
build.artifacts_file.path
end
- def delete_artifact!
- build.reload # Reload stable object to prevent erase artifacts with old state
- build.erase_artifacts! unless build.has_expiring_artifacts?
- end
-
def latest_sha
project.commit(build.ref).try(:sha).to_s
ensure
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
new file mode 100644
index 00000000000..8183a2f26d7
--- /dev/null
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -0,0 +1,30 @@
+module Projects
+ class UpdateRemoteMirrorService < BaseService
+ attr_reader :errors
+
+ def execute(remote_mirror)
+ @errors = []
+
+ return success unless remote_mirror.enabled?
+
+ begin
+ repository.fetch_remote(remote_mirror.remote_name, no_tags: true)
+
+ opts = {}
+ if remote_mirror.only_protected_branches?
+ opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
+ end
+
+ remote_mirror.update_repository(opts)
+ rescue => e
+ errors << e.message.strip
+ end
+
+ if errors.present?
+ error(errors.join("\n\n"))
+ else
+ success
+ end
+ end
+ end
+end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 6cc51b6ee1b..0215994b1a7 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -138,8 +138,10 @@ module QuickActions
'Remove assignee'
end
end
- explanation do
- "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
+ explanation do |users = nil|
+ assignees = issuable.assignees
+ assignees &= users if users.present? && issuable.allows_multiple_assignees?
+ "Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
end
params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
@@ -268,6 +270,26 @@ module QuickActions
end
end
+ desc 'Copy labels and milestone from other issue or merge request'
+ explanation do |source_issuable|
+ "Copy labels and milestone from #{source_issuable.to_reference}."
+ end
+ params '#issue | !merge_request'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ parse_params do |issuable_param|
+ extract_references(issuable_param, :issue).first ||
+ extract_references(issuable_param, :merge_request).first
+ end
+ command :copy_metadata do |source_issuable|
+ if source_issuable.present? && source_issuable.project.id == issuable.project.id
+ @updates[:add_label_ids] = source_issuable.labels.map(&:id)
+ @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
+ end
+ end
+
desc 'Add a todo'
explanation 'Adds a todo.'
condition do
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
index 9a88459b511..ba7be4b3f89 100644
--- a/app/services/repository_archive_clean_up_service.rb
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -20,11 +20,12 @@ class RepositoryArchiveCleanUpService
private
def clean_up_old_archives
- run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete))
+ run(%W(find #{path} -mindepth 1 -maxdepth 3 -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -mmin +#{mmin} -delete))
end
def clean_up_empty_directories
- run(%W(find #{path} -not -path #{path} -type d -empty -name \*.git -maxdepth 1 -delete))
+ run(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -empty -delete))
+ run(%W(find #{path} -mindepth 1 -maxdepth 1 -type d -empty -delete))
end
def run(cmd)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 958ef065012..00bf5434b7f 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0
"removed time estimate"
else
- "changed time estimate to #{parsed_time},"
+ "changed time estimate to #{parsed_time}"
end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index ffd48e842c2..f91cd03bf5c 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -98,12 +98,12 @@ class TodoService
# When a build fails on the HEAD of a merge request we should:
#
- # * create a todo for author of MR to fix it
- # * create a todo for merge_user to keep an eye on it
+ # * create a todo for each merge participant
#
def merge_request_build_failed(merge_request)
- create_build_failed_todo(merge_request, merge_request.author)
- create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
+ merge_request.merge_participants.each do |user|
+ create_build_failed_todo(merge_request, user)
+ end
end
# When a new commit is pushed to a merge request we should:
@@ -116,20 +116,22 @@ class TodoService
# When a build is retried to a merge request we should:
#
- # * mark all pending todos related to the merge request for the author as done
- # * mark all pending todos related to the merge request for the merge_user as done
+ # * mark all pending todos related to the merge request as done for each merge participant
#
def merge_request_build_retried(merge_request)
- mark_pending_todos_as_done(merge_request, merge_request.author)
- mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
+ merge_request.merge_participants.each do |user|
+ mark_pending_todos_as_done(merge_request, user)
+ end
end
- # When a merge request could not be automatically merged due to its unmergeable state we should:
+ # When a merge request could not be merged due to its unmergeable state we should:
#
- # * create a todo for a merge_user
+ # * create a todo for each merge participant
#
def merge_request_became_unmergeable(merge_request)
- create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
+ merge_request.merge_participants.each do |user|
+ create_unmergeable_todo(merge_request, user)
+ end
end
# When create a note we should:
@@ -275,9 +277,9 @@ class TodoService
create_todos(todo_author, attributes)
end
- def create_unmergeable_todo(merge_request, merge_user)
- attributes = attributes_for_todo(merge_request.project, merge_request, merge_user, Todo::UNMERGEABLE)
- create_todos(merge_user, attributes)
+ def create_unmergeable_todo(merge_request, todo_author)
+ attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::UNMERGEABLE)
+ create_todos(todo_author, attributes)
end
def attributes_for_target(target)
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index 976017dfa82..a2833b1e051 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -49,7 +49,7 @@ module Users
migrate_merge_requests
migrate_notes
migrate_abuse_reports
- migrate_award_emojis
+ migrate_award_emoji
end
def migrate_issues
@@ -70,7 +70,7 @@ module Users
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
end
- def migrate_award_emojis
+ def migrate_award_emoji
user.award_emoji.update_all(user_id: ghost_user.id)
end
end
diff --git a/app/services/users/respond_to_terms_service.rb b/app/services/users/respond_to_terms_service.rb
new file mode 100644
index 00000000000..06d660186cf
--- /dev/null
+++ b/app/services/users/respond_to_terms_service.rb
@@ -0,0 +1,24 @@
+module Users
+ class RespondToTermsService
+ def initialize(user, term)
+ @user, @term = user, term
+ end
+
+ def execute(accepted:)
+ agreement = @user.term_agreements.find_or_initialize_by(term: @term)
+ agreement.accepted = accepted
+
+ if agreement.save
+ store_accepted_term(accepted)
+ end
+
+ agreement
+ end
+
+ private
+
+ def store_accepted_term(accepted)
+ @user.update_column(:accepted_term_id, accepted ? @term.id : nil)
+ end
+ end
+end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 809ce1303d8..7ec52b6ce2b 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -41,7 +41,7 @@ class WebHookService
http_status: response.code,
message: response.to_s
}
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout => e
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError => e
log_execution(
trigger: hook_name,
url: hook.url,
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index f12f0466a1d..f8a237178d9 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
!!model
end
+ def local_url
+ File.join('/', self.class.base_dir, dynamic_segment, filename)
+ end
+
private
# Designed to be overridden by child uploaders that have a dynamic path
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index dd86753479d..2a5a830ce4f 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -6,10 +6,10 @@ class JobArtifactUploader < GitlabUploader
storage_options Gitlab.config.artifacts
- def size
- return super if model.size.nil?
+ def cached_size
+ return model.size if model.size.present? && !model.file_changed?
- model.size
+ size
end
def store_dir
@@ -20,7 +20,7 @@ class JobArtifactUploader < GitlabUploader
if file_storage?
File.open(path, "rb") if path
else
- ::Gitlab::Ci::Trace::HttpIO.new(url, size) if url
+ ::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index bd258e04d3f..f2a8afccdeb 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -103,6 +103,7 @@ module ObjectStorage
end
included do
+ include AfterCommitQueue
after_save on: [:create, :update] do
background_upload(changed_mounts)
end
@@ -183,14 +184,6 @@ module ObjectStorage
StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options)
}
end
-
- def default_object_store
- if self.object_store_enabled? && self.direct_upload_enabled?
- Store::REMOTE
- else
- Store::LOCAL
- end
- end
end
# allow to configure and overwrite the filename
@@ -211,12 +204,13 @@ module ObjectStorage
end
def object_store
- @object_store ||= model.try(store_serialization_column) || self.class.default_object_store
+ # We use Store::LOCAL as null value indicates the local storage
+ @object_store ||= model.try(store_serialization_column) || Store::LOCAL
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def object_store=(value)
- @object_store = value || self.class.default_object_store
+ @object_store = value || Store::LOCAL
@storage = storage_for(object_store)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -302,6 +296,15 @@ module ObjectStorage
super
end
+ def store!(new_file = nil)
+ # when direct upload is enabled, always store on remote storage
+ if self.class.object_store_enabled? && self.class.direct_upload_enabled?
+ self.object_store = Store::REMOTE
+ end
+
+ super
+ end
+
private
def schedule_background_upload?
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
index d50b4071745..5abb0dc9182 100644
--- a/app/views/abuse_report_mailer/notify.html.haml
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -4,7 +4,7 @@
#{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
(@#{@abuse_report.reporter.username}).
-%blockquote
+.blockquote
= @abuse_report.message
%p
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index 06be1a53318..278ad210543 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -2,20 +2,20 @@
%h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr
-= form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f|
+= form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f|
= form_errors(@abuse_report)
= f.hidden_field :user_id
- .form-group
- = f.label :user_id, class: 'control-label'
+ .form-group.row
+ = f.label :user_id, class: 'col-sm-2 col-form-label'
.col-sm-10
- name = "#{@abuse_report.user.name} (@#{@abuse_report.user.username})"
= text_field_tag :user_name, name, class: "form-control", readonly: true
- .form-group
- = f.label :message, class: 'control-label'
+ .form-group.row
+ = f.label :message, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :message, class: "form-control", rows: 2, required: true, value: sanitize(@ref_url)
- .help-block
+ .form-text.text-muted
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
.form-actions
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 18c6c559049..89a87f38968 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -1,7 +1,7 @@
- reporter = abuse_report.reporter
- user = abuse_report.user
%tr
- %th.visible-xs-block.visible-sm-block
+ %th.d-block.d-sm-none.d-md-none
%strong User
%td
- if user
@@ -11,7 +11,7 @@
- else
(removed)
%td
- %strong.subheading.visible-xs-block.visible-sm-block Reported by
+ %strong.subheading.d-block.d-sm-none.d-md-none Reported by
- if reporter
= link_to reporter.name, reporter
- else
@@ -19,7 +19,7 @@
.light.small
= time_ago_with_tooltip(abuse_report.created_at)
%td
- %strong.subheading.visible-xs-block.visible-sm-block Message
+ %strong.subheading.d-block.d-sm-none.d-md-none Message
.message
= markdown_field(abuse_report, :message)
%td
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 6a5e170ddd8..cc29657a439 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -5,7 +5,7 @@
- if @abuse_reports.present?
.table-holder
%table.table.responsive-table
- %thead.hidden-sm.hidden-xs
+ %thead.d-none.d-sm-none.d-md-table-header-group
%tr
%th User
%th Reported by
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 15bda97c3b5..5e08837255f 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -1,11 +1,11 @@
-= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
+= form_for @appearance, url: admin_appearances_path do |f|
= form_errors(@appearance)
%fieldset.app_logo
%legend
Navigation bar:
- .form-group
- = f.label :header_logo, 'Header logo', class: 'control-label'
+ .form-group.row
+ = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label'
.col-sm-10
- if @appearance.header_logo?
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
@@ -21,18 +21,18 @@
%fieldset.sign-in
%legend
Sign in/Sign up pages:
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_field :title, class: "form-control"
- .form-group
- = f.label :description, class: 'control-label'
+ .form-group.row
+ = f.label :description, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :description, class: "form-control", rows: 10
.hint
Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
- .form-group
- = f.label :logo, class: 'control-label'
+ .form-group.row
+ = f.label :logo, class: 'col-sm-2 col-form-label'
.col-sm-10
- if @appearance.logo?
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
@@ -48,8 +48,8 @@
%fieldset
%legend
New project pages:
- .form-group
- = f.label :new_project_guidelines, class: 'control-label'
+ .form-group.row
+ = f.label :new_project_guidelines, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :new_project_guidelines, class: "form-control", rows: 10
.hint
@@ -63,5 +63,5 @@
= link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
- %span.pull-right
+ %span.float-right
Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
index bb3fa26a33e..6b9b2a17dd9 100644
--- a/app/views/admin/application_settings/_abuse.html.haml
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -1,12 +1,12 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :admin_notification_email, 'Abuse reports notification email', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :admin_notification_email, class: 'form-control'
- .help-block
+ .form-text.text-muted
Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index dd86c9ed2eb..f40dd347eb3 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -1,37 +1,37 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :gravatar_enabled do
= f.check_box :gravatar_enabled
Gravatar enabled
- .form-group
- = f.label :default_projects_limit, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :default_projects_limit, class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :default_projects_limit, class: 'form-control'
- .form-group
- = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :max_attachment_size, class: 'form-control'
- .form-group
- = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :session_expire_delay, 'Session duration (minutes)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :session_expire_delay, class: 'form-control'
- %span.help-block#session_expire_delay_help_block GitLab restart is required to apply changes
- .form-group
- = f.label :user_oauth_applications, 'User OAuth applications', class: 'control-label col-sm-2'
+ %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes
+ .form-group.row
+ = f.label :user_oauth_applications, 'User OAuth applications', class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
+ .form-check
= f.label :user_oauth_applications do
= f.check_box :user_oauth_applications
Allow users to register any application to use GitLab as an OAuth provider
- .form-group
- = f.label :user_default_external, 'New users set to external', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :user_default_external, 'New users set to external', class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
+ .form-check
= f.label :user_default_external do
= f.check_box :user_default_external
Newly registered users will by default be external
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
index 8198a822a10..da7248337d2 100644
--- a/app/views/admin/application_settings/_background_jobs.html.haml
+++ b/app/views/admin/application_settings/_background_jobs.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -6,25 +6,25 @@
These settings require a
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :sidekiq_throttling_enabled do
= f.check_box :sidekiq_throttling_enabled
Enable Sidekiq Job Throttling
- .help-block
+ .form-text.text-muted
Limit the amount of resources slow running jobs are assigned.
- .form-group
- = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'col-form-label col-sm-2'
.col-sm-10
= f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
- .help-block
+ .form-text.text-muted
Choose which queues you wish to throttle.
- .form-group
- = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
- .help-block
+ .form-text.text-muted
The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
= f.submit 'Save changes', 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 b4d2a789df0..6a06271de2a 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -1,45 +1,45 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :auto_devops_enabled do
= f.check_box :auto_devops_enabled
- Enabled Auto DevOps (Beta) for projects by default
- .help-block
+ Enabled Auto DevOps for projects by default
+ .form-text.text-muted
It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
- .form-group
- = f.label :auto_devops_domain, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :auto_devops_domain, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com'
- .help-block
+ .form-text.text-muted
= s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.")
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :shared_runners_enabled do
= f.check_box :shared_runners_enabled
Enable shared runners for new projects
- .form-group
- = f.label :shared_runners_text, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :shared_runners_text, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :shared_runners_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
- .form-group
- = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
+ .form-text.text-muted Markdown enabled
+ .form-group.row
+ = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
- .help-block
+ .form-text.text-muted
Set the maximum file size for each job's artifacts
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size')
- .form-group
- = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :default_artifacts_expire_in, class: 'form-control'
- .help-block
+ .form-text.text-muted
Set the default expiration time for each job's artifacts.
0 for unlimited.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index 6c89f1c4e98..7443f02ad15 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -1,24 +1,24 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :email_author_in_body do
= f.check_box :email_author_in_body
Include author name in notification email body
- .help-block
+ .form-text.text-muted
Some email servers do not support overriding the email sender name.
Enable this option to include the name of the author of the issue,
merge request or comment in the email body instead.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :html_emails_enabled do
= f.check_box :html_emails_enabled
Enable HTML emails
- .help-block
+ .form-text.text-muted
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.
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
index 4acc5b3a0c5..859a1c6f45c 100644
--- a/app/views/admin/application_settings/_gitaly.html.haml
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -1,27 +1,27 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_default, class: 'form-control'
- .help-block
+ .form-text.text-muted
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
for git fetch/push operations or Sidekiq jobs.
- .form-group
- = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_fast, class: 'form-control'
- .help-block
+ .form-text.text-muted
Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
can help maintain the stability of the GitLab instance.
- .form-group
- = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :gitaly_timeout_medium, class: 'form-control'
- .help-block
+ .form-text.text-muted
Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
index 3bc101ddf04..97d397788c6 100644
--- a/app/views/admin/application_settings/_help_page.html.haml
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -1,22 +1,22 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :help_page_text, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :help_page_text, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :help_page_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-text.text-muted Markdown enabled
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :help_page_hide_commercial_content do
= f.check_box :help_page_hide_commercial_content
Hide marketing-related entries from help
- .form-group
- = f.label :help_page_support_url, 'Support page URL', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :help_page_support_url, 'Support page URL', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
- %span.help-block#support_help_block Alternate support URL for help page
+ %span.form-text.text-muted#support_help_block Alternate support URL for help page
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml
index a173fd38a9c..e5d699f9a2f 100644
--- a/app/views/admin/application_settings/_influx.html.haml
+++ b/app/views/admin/application_settings/_influx.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -8,60 +8,60 @@
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :metrics_enabled do
= f.check_box :metrics_enabled
Enable InfluxDB Metrics
- .form-group
- = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :metrics_host, 'InfluxDB host', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
- .form-group
- = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :metrics_port, 'InfluxDB port', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
- .help-block
+ .form-text.text-muted
The UDP port to use for connecting to InfluxDB. InfluxDB requires that
your server configuration specifies a database to store data in when
sending messages to this port, without it metrics data will not be
saved.
- .form-group
- = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :metrics_pool_size, 'Connection pool size', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :metrics_pool_size, class: 'form-control'
- .help-block
+ .form-text.text-muted
The amount of InfluxDB connections to open. Connections are opened
lazily. Users using multi-threaded application servers should ensure
enough connections are available (at minimum the amount of application
server threads).
- .form-group
- = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :metrics_timeout, 'Connection timeout', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :metrics_timeout, class: 'form-control'
- .help-block
+ .form-text.text-muted
The amount of seconds after which an InfluxDB connection will time
out.
- .form-group
- = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :metrics_method_call_threshold, class: 'form-control'
- .help-block
+ .form-text.text-muted
A method call is only tracked when it takes longer to complete than
the given amount of milliseconds.
- .form-group
- = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :metrics_sample_interval, class: 'form-control'
- .help-block
+ .form-text.text-muted
The sampling interval in seconds. Sampled data includes memory usage,
retained Ruby objects, file descriptors and so on.
- .form-group
- = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :metrics_packet_size, 'Metrics per packet', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :metrics_packet_size, class: 'form-control'
- .help-block
+ .form-text.text-muted
The amount of points to store in a single UDP packet. More points
results in fewer but larger UDP packets being sent.
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index b83ffc375d9..539ff9b5168 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -1,53 +1,53 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :throttle_unauthenticated_enabled do
= f.check_box :throttle_unauthenticated_enabled
Enable unauthenticated request rate limit
- %span.help-block
+ %span.form-text.text-muted
Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group
- = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
- .form-group
- = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :throttle_authenticated_api_enabled do
= f.check_box :throttle_authenticated_api_enabled
Enable authenticated API request rate limit
- %span.help-block
+ %span.form-text.text-muted
Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group
- = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
- .form-group
- = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :throttle_authenticated_web_enabled do
= f.check_box :throttle_authenticated_web_enabled
Enable authenticated web request rate limit
- %span.help-block
+ %span.form-text.text-muted
Helps reduce request volume (e.g. from crawlers or abusive bots)
- .form-group
- = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
- .form-group
- = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml
index 17358cf775b..0532f49fd5b 100644
--- a/app/views/admin/application_settings/_koding.html.haml
+++ b/app/views/admin/application_settings/_koding.html.haml
@@ -1,20 +1,20 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :koding_enabled do
= f.check_box :koding_enabled
Enable Koding
- .help-block
+ .form-text.text-muted
Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
- .form-group
- = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :koding_url, 'Koding URL', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
- .help-block
+ .form-text.text-muted
Koding has integration enabled out of the box for the
%strong gitlab
team, and you need to provide that team's URL here. Learn more in the
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
index 44a11ddc120..4341801b045 100644
--- a/app/views/admin/application_settings/_logging.html.haml
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -1,35 +1,35 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :sentry_enabled do
= f.check_box :sentry_enabled
Enable Sentry
- .help-block
+ .form-text.text-muted
%p This setting requires a restart to take effect.
Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
%a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
- .form-group
- = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :sentry_dsn, 'Sentry DSN', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :sentry_dsn, class: 'form-control'
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :clientside_sentry_enabled do
= f.check_box :clientside_sentry_enabled
Enable Clientside Sentry
- .help-block
+ .form-text.text-muted
Sentry can also be used for reporting and logging clientside exceptions.
%a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
- .form-group
- = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :clientside_sentry_dsn, class: 'form-control'
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index d10f609006d..176cdfe9e1a 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -1,10 +1,10 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :allow_local_requests_from_hooks_and_services do
= f.check_box :allow_local_requests_from_hooks_and_services
Allow requests to the local network from hooks and services
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index b28ecf9a039..0100ef038e2 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -1,19 +1,19 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :max_pages_size, class: 'form-control'
- .help-block 0 for unlimited
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-text.text-muted 0 for unlimited
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :pages_domain_verification_enabled do
= f.check_box :pages_domain_verification_enabled
Require users to prove ownership of custom domains
- .help-block
+ .form-text.text-muted
Domain verification is an essential security measure for public GitLab
sites. Users are required to demonstrate they control a domain before
it is enabled
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index 01d5a31aa9f..07d3a8b7cdf 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -1,14 +1,14 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :authorized_keys_enabled do
= f.check_box :authorized_keys_enabled
Write to "authorized_keys" file
- .help-block
+ .form-text.text-muted
By default, we write to the "authorized_keys" file to support Git
over SSH without additional configuration. GitLab can be optimized
to authenticate SSH keys via the database file. Only uncheck this
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index 5344f030c97..8001b42e2f9 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -1,15 +1,15 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :performance_bar_enabled do
= f.check_box :performance_bar_enabled
Enable the Performance Bar
- .form-group
- = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index 56764b3fb81..ee6d1d1a888 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -1,18 +1,18 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :plantuml_enabled do
= f.check_box :plantuml_enabled
Enable PlantUML
- .form-group
- = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :plantuml_url, 'PlantUML URL', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
- .help-block
+ .form-text.text-muted
Allow rendering of
= link_to "PlantUML", "http://plantuml.com"
diagrams in Asciidoc documents using an external PlantUML service.
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index 48745db2991..8c95597e787 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -11,14 +11,14 @@
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
= link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :prometheus_metrics_enabled do
= f.check_box :prometheus_metrics_enabled
Enable Prometheus Metrics
- unless Gitlab::Metrics.metrics_folder_present?
- .help-block
+ .form-text.text-muted
%strong.cred WARNING:
Environment variable
%code prometheus_multiproc_dir
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
index 0a53a75119e..63a592cc2fd 100644
--- a/app/views/admin/application_settings/_realtime.html.haml
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -1,12 +1,12 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :polling_interval_multiplier, class: 'form-control'
- .help-block
+ .form-text.text-muted
Change this value to influence how frequently the GitLab UI polls for updates.
If you set the value to 2 all polling intervals are multiplied
by 2, which means that polling happens half as frequently.
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index 3451ef62458..8524cbfe4d9 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -1,9 +1,9 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index f33769b23c2..739ed359e16 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -1,62 +1,62 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.sub-section
%h4 Repository checks
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :repository_checks_enabled do
= f.check_box :repository_checks_enabled
Enable Repository Checks
- .help-block
+ .form-text.text-muted
GitLab will periodically run
- %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
+ %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
- .help-block
+ .form-text.text-muted
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
.sub-section
%h4 Housekeeping
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :housekeeping_enabled do
= f.check_box :housekeeping_enabled
Enable automatic repository housekeeping (git repack, git gc)
- .help-block
+ .form-text.text-muted
If you keep automatic housekeeping disabled for a long time Git
repository access on your GitLab server will become slower and your
repositories will use more disk space. We recommend to always leave
this enabled.
- .checkbox
+ .form-check
= f.label :housekeeping_bitmaps_enabled do
= f.check_box :housekeeping_bitmaps_enabled
Enable Git pack file bitmap creation
- .help-block
+ .form-text.text-muted
Creating pack file bitmaps makes housekeeping take a little longer but
bitmaps should accelerate 'git clone' performance.
- .form-group
- = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
- .help-block
+ .form-text.text-muted
Number of Git pushes after which an incremental 'git repack' is run.
- .form-group
- = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :housekeeping_full_repack_period, class: 'form-control'
- .help-block
+ .form-text.text-muted
Number of Git pushes after which a full 'git repack' is run.
- .form-group
- = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :housekeeping_gc_period, 'Git GC period', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :housekeeping_gc_period, class: 'form-control'
- .help-block
+ .form-text.text-muted
Number of Git pushes after which 'git gc' is run.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
new file mode 100644
index 00000000000..9d05a5aa234
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -0,0 +1,16 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.label :mirror_available do
+ = f.check_box :mirror_available
+ Allow mirrors to be setup for projects
+ %span.help-block
+ If disabled, only admins will be able to setup mirrors in projects.
+ = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index ac31977e1a9..be85210934c 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -1,58 +1,58 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.sub-section
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :hashed_storage_enabled do
= f.check_box :hashed_storage_enabled
Create new projects using hashed storage paths
- .help-block
+ .form-text.text-muted
Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
%em (EXPERIMENTAL)
- .form-group
- = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :repository_storages, 'Storage paths for new projects', class: 'col-form-label col-sm-2'
.col-sm-10
= f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
{include_hidden: false}, multiple: true, class: 'form-control'
- .help-block
+ .form-text.text-muted
Manage repository storage paths. Learn more in the
= succeed "." do
- = link_to "repository storages documentation", help_page_path("administration/repository_storages")
+ = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section
%h4 Circuit breaker
- .form-group
- = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_check_interval, class: 'form-control'
- .help-block
+ .form-text.text-muted
= circuitbreaker_check_interval_help_text
- .form-group
- = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_access_retries, class: 'form-control'
- .help-block
+ .form-text.text-muted
= circuitbreaker_access_retries_help_text
- .form-group
- = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
- .help-block
+ .form-text.text-muted
= circuitbreaker_storage_timeout_help_text
- .form-group
- = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
- .help-block
+ .form-text.text-muted
= circuitbreaker_failure_count_help_text
- .form-group
- = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
- .help-block
+ .form-text.text-muted
= circuitbreaker_failure_reset_time_help_text
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 48331c40bca..4d74568d69a 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -1,60 +1,60 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :password_authentication_enabled_for_web do
= f.check_box :password_authentication_enabled_for_web
Password authentication enabled for web interface
- .help-block
+ .form-text.text-muted
When disabled, an external authentication provider must be used.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :password_authentication_enabled_for_git do
= f.check_box :password_authentication_enabled_for_git
Password authentication enabled for Git over HTTP(S)
- .help-block
+ .form-text.text-muted
When disabled, a Personal Access Token
- if Gitlab::Auth::LDAP::Config.enabled?
or LDAP password
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
- .form-group
+ .form-group.row
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
.col-sm-10
.btn-group{ data: { toggle: 'buttons' } }
- oauth_providers_checkboxes.each do |source|
= source
- .form-group
- = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
+ .form-check
= f.label :require_two_factor_authentication do
= f.check_box :require_two_factor_authentication
Require all users to setup Two-factor authentication
- .form-group
- = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
- .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
- .form-group
- = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
+ .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ .form-group.row
+ = f.label :home_page_url, 'Home page URL', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
- %span.help-block#home_help_block We will redirect non-logged in users to this page
- .form-group
- = f.label :after_sign_out_path, class: 'control-label col-sm-2'
+ %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
+ .form-group.row
+ = f.label :after_sign_out_path, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
- %span.help-block#after_sign_out_path_help_block We will redirect users to this page after they sign out
- .form-group
- = f.label :sign_in_text, class: 'control-label col-sm-2'
+ %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
+ .form-group.row
+ = f.label :sign_in_text, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :sign_in_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
+ .form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
index 85f311dd894..50c455f8686 100644
--- a/app/views/admin/application_settings/_signup.html.haml
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -1,58 +1,58 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :signup_enabled do
= f.check_box :signup_enabled
Sign-up enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :send_user_confirmation_email do
= f.check_box :send_user_confirmation_email
Send confirmation email on sign-up
- .form-group
- = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- .form-group
- = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
+ .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ .form-group.row
+ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'col-form-label col-sm-2'
.col-sm-10
- .checkbox
+ .form-check
= f.label :domain_blacklist_enabled do
= f.check_box :domain_blacklist_enabled
Enable domain blacklist for sign ups
- .form-group
- .col-sm-offset-2.col-sm-10
- .radio
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= label_tag :blacklist_type_file do
= radio_button_tag :blacklist_type, :file
.option-title
Upload blacklist file
- .radio
+ .form-check
= label_tag :blacklist_type_raw do
= radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
.option-title
Enter blacklist manually
- .form-group.blacklist-file
- = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
+ .form-group.row.blacklist-file
+ = f.label :domain_blacklist_file, 'Blacklist file', class: 'col-form-label col-sm-2'
.col-sm-10
= f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
- .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
- .form-group.blacklist-raw
- = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
+ .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
+ .form-group.row.blacklist-raw
+ = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
- .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- .form-group
- = f.label :after_sign_up_text, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :after_sign_up_text, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
+ .form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index 25e89097dfe..58543a0359a 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -1,65 +1,65 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :recaptcha_enabled do
= f.check_box :recaptcha_enabled
Enable reCAPTCHA
- %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
+ %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts
- .form-group
- = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_site_key, class: 'form-control'
- .help-block
+ .form-text.text-muted
Generate site and private keys at
%a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
- .form-group
- = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_private_key, class: 'form-control'
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :akismet_enabled do
= f.check_box :akismet_enabled
Enable Akismet
- %span.help-block#akismet_help_block Helps prevent bots from creating issues
+ %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues
- .form-group
- = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :akismet_api_key, 'Akismet API Key', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :akismet_api_key, class: 'form-control'
- .help-block
+ .form-text.text-muted
Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :unique_ips_limit_enabled do
= f.check_box :unique_ips_limit_enabled
Limit sign in from multiple ips
- %span.help-block#unique_ip_help_block
+ %span.form-text.text-muted#unique_ip_help_block
Helps prevent malicious users hide their activity
- .form-group
- = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_per_user, class: 'form-control'
- .help-block
+ .form-text.text-muted
Maximum number of unique IPs per user
- .form-group
- = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_time_window, class: 'form-control'
- .help-block
+ .form-text.text-muted
How many seconds an IP will be counted towards the limit
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index 36d8838803f..ae02d07e556 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -1,12 +1,12 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :terminal_max_session_time, 'Max session time', class: 'col-form-label col-sm-2'
.col-sm-10
= f.number_field :terminal_max_session_time, class: 'form-control'
- .help-block
+ .form-text.text-muted
Maximum time for web terminal websocket connection (in seconds).
0 for unlimited.
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
new file mode 100644
index 00000000000..32b060972ec
--- /dev/null
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -0,0 +1,22 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .col-sm-12
+ .form-check
+ = f.label :enforce_terms do
+ = f.check_box :enforce_terms
+ = _("Require all users to accept Terms of Service when they access GitLab.")
+ .help-block
+ = _("When enabled, users cannot use GitLab until the terms have been accepted.")
+ .form-group
+ .col-sm-12
+ = f.label :terms do
+ = _("Terms of Service Agreement")
+ .col-sm-12
+ = f.text_area :terms, class: 'form-control', rows: 8
+ .help-block
+ = _("Markdown enabled")
+
+ = f.submit _("Save changes"), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 7684e2cfdd1..316c8b04dea 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -1,25 +1,25 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Enable version check
- .help-block
+ .form-text.text-muted
GitLab will inform you if a new version is available.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
about what information is shared with GitLab Inc.
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
- can_be_configured = @application_setting.usage_ping_can_be_configured?
- .checkbox
+ .form-check
= f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured
Enable usage ping
- .help-block
+ .form-text.text-muted
- if can_be_configured
To help improve GitLab and its user experience, GitLab will
periodically collect usage information.
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index a75dd90fe6b..c37a89237f0 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -1,41 +1,41 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
- .form-group
- = f.label :default_branch_protection, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :default_branch_protection, class: 'col-form-label col-sm-2'
.col-sm-10
= f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
- .form-group.visibility-level-setting
- = f.label :default_project_visibility, class: 'control-label col-sm-2'
+ .form-group.row.visibility-level-setting
+ = f.label :default_project_visibility, class: 'col-form-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
- .form-group.visibility-level-setting
- = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
+ .form-group.row.visibility-level-setting
+ = f.label :default_snippet_visibility, class: 'col-form-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
- .form-group.visibility-level-setting
- = f.label :default_group_visibility, class: 'control-label col-sm-2'
+ .form-group.row.visibility-level-setting
+ = f.label :default_group_visibility, class: 'col-form-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
- .form-group
- = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :restricted_visibility_levels, class: 'col-form-label col-sm-2'
.col-sm-10
- checkbox_name = 'application_setting[restricted_visibility_levels][]'
= hidden_field_tag(checkbox_name)
- restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
- .checkbox
+ .form-check
= level
- %span.help-block#restricted-visibility-help
+ %span.form-text.text-muted#restricted-visibility-help
Selected levels cannot be used by non-admin users for projects or snippets.
If the public level is restricted, user profiles are only visible to logged in users.
- .form-group
- = f.label :import_sources, class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label :import_sources, class: 'col-form-label col-sm-2'
.col-sm-10
= hidden_field_tag 'application_setting[import_sources][]'
- import_sources_checkboxes('import-sources-help').each do |source|
- .checkbox= source
- %span.help-block#import-sources-help
+ .form-check= source
+ %span.form-text.text-muted#import-sources-help
Enabled sources for code import during project creation. OmniAuth must be configured for GitHub
= link_to "(?)", help_page_path("integration/github")
, Bitbucket
@@ -43,24 +43,24 @@
and GitLab.com
= link_to "(?)", help_page_path("integration/gitlab")
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= f.label :project_export_enabled do
= f.check_box :project_export_enabled
Project export enabled
- .form-group
- %label.control-label.col-sm-2 Enabled Git access protocols
+ .form-group.row
+ %label.col-form-label.col-sm-2 Enabled Git access protocols
.col-sm-10
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
- %span.help-block#clone-protocol-help
+ %span.form-text.text-muted#clone-protocol-help
Allow only the selected protocols to be used for Git access.
- ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
- field_name = :"#{type}_key_restriction"
- .form-group
- = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
+ .form-group.row
+ = f.label field_name, "#{type.upcase} SSH keys", class: 'col-form-label col-sm-2'
.col-sm-10
= f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index caaa93aa1e2..3f440c76ee0 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -8,7 +8,7 @@
%h4
= _('Visibility and access controls')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
.settings-content
@@ -19,7 +19,7 @@
%h4
= _('Account and limit settings')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Session expiration, projects limit and attachment size.')
.settings-content
@@ -30,7 +30,7 @@
%h4
= _('Sign-up restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure the way a user creates a new account.')
.settings-content
@@ -41,18 +41,29 @@
%h4
= _('Sign-in restrictions')
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
.settings-content
= render 'signin'
+%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Terms of Service')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Include a Terms of Service agreement that all users must accept.')
+ .settings-content
+ = render 'terms'
+
%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Help page')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Help page text and support page url.')
.settings-content
@@ -62,8 +73,8 @@
.settings-header
%h4
= _('Pages')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Size and domain settings for static websites')
.settings-content
@@ -73,8 +84,8 @@
.settings-header
%h4
= _('Continuous Integration and Deployment')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Auto DevOps, runners and job artifacts')
.settings-content
@@ -84,8 +95,8 @@
.settings-header
%h4
= _('Metrics - Influx')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable and configure InfluxDB metrics.')
.settings-content
@@ -95,8 +106,8 @@
.settings-header
%h4
= _('Metrics - Prometheus')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable and configure Prometheus metrics.')
.settings-content
@@ -106,8 +117,8 @@
.settings-header
%h4
= _('Profiling - Performance bar')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable the Performance Bar for a given group.')
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
@@ -118,8 +129,8 @@
.settings-header
%h4
= _('Background jobs')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure Sidekiq job throttling.')
.settings-content
@@ -129,8 +140,8 @@
.settings-header
%h4
= _('Spam and Anti-bot Protection')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable reCAPTCHA or Akismet and set IP limits.')
.settings-content
@@ -140,8 +151,8 @@
.settings-header
%h4
= _('Abuse reports')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set notification email for abuse reports.')
.settings-content
@@ -151,8 +162,8 @@
.settings-header
%h4
= _('Error Reporting and Logging')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable Sentry for error reporting and logging.')
.settings-content
@@ -162,8 +173,8 @@
.settings-header
%h4
= _('Repository storage')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure storage path and circuit breaker settings.')
.settings-content
@@ -173,8 +184,8 @@
.settings-header
%h4
= _('Repository maintenance')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure automatic git checks and housekeeping on repositories.')
.settings-content
@@ -185,8 +196,8 @@
.settings-header
%h4
= _('Container Registry')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various container registry settings.')
.settings-content
@@ -197,8 +208,8 @@
.settings-header
%h4
= _('Koding')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Online IDE integration settings.')
.settings-content
@@ -208,8 +219,8 @@
.settings-header
%h4
= _('PlantUML')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
.settings-content
@@ -219,8 +230,8 @@
.settings-header#usage-statistics
%h4
= _('Usage statistics')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Enable or disable version check and usage ping.')
.settings-content
@@ -230,8 +241,8 @@
.settings-header
%h4
= _('Email')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various email settings.')
.settings-content
@@ -241,8 +252,8 @@
.settings-header
%h4
= _('Gitaly')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure Gitaly timeouts.')
.settings-content
@@ -252,8 +263,8 @@
.settings-header
%h4
= _('Web terminal')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Set max session time for web terminal.')
.settings-content
@@ -263,8 +274,8 @@
.settings-header
%h4
= _('Real-time features')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Change this value to influence how frequently the GitLab UI polls for updates.')
.settings-content
@@ -274,8 +285,8 @@
.settings-header
%h4
= _('Performance optimization')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Various settings that affect GitLab performance.')
.settings-content
@@ -285,8 +296,8 @@
.settings-header
%h4
= _('User and IP Rate Limits')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Configure limits for web and API requests.')
.settings-content
@@ -296,9 +307,20 @@
.settings-header
%h4
= _('Outbound requests')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
%p
= _('Allow requests to the local network from hooks and services.')
.settings-content
= render 'outbound'
+
+%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Repository mirror settings')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Configure push mirrors.')
+ .settings-content
+ = render partial: 'repository_mirrors_form'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 93827d6a1ab..b5d79e1da13 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -1,34 +1,34 @@
-= form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f|
+= form_for [:admin, @application], url: @url, html: {role: 'form'} do |f|
= form_errors(application)
= content_tag :div, class: 'form-group' do
- = f.label :name, class: 'col-sm-2 control-label'
+ = f.label :name, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_field :name, class: 'form-control'
= doorkeeper_errors_for application, :name
= content_tag :div, class: 'form-group' do
- = f.label :redirect_uri, class: 'col-sm-2 control-label'
+ = f.label :redirect_uri, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.text_area :redirect_uri, class: 'form-control'
= doorkeeper_errors_for application, :redirect_uri
- %span.help-block
+ %span.form-text.text-muted
Use one line per URI
- if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
+ %span.form-text.text-muted
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
= content_tag :div, class: 'form-group' do
- = f.label :trusted, class: 'col-sm-2 control-label'
+ = f.label :trusted, class: 'col-sm-2 col-form-label'
.col-sm-10
= f.check_box :trusted
- %span.help-block
+ %span.form-text.text-muted
Trusted applications are automatically authorized on GitLab OAuth flow.
- .form-group
- = f.label :scopes, class: 'col-sm-2 control-label'
+ .form-group.row
+ = f.label :scopes, class: 'col-sm-2 col-form-label'
.col-sm-10
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 5125aa21b06..593a6d816e3 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -32,5 +32,5 @@
= render "shared/tokens/scopes_list", token: @application
.form-actions
- = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
+ = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide float-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index f0cc4d7ee62..faa5854bb40 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -7,9 +7,9 @@
%hr
- .panel.panel-default
- .panel-heading Sidekiq running processes
- .panel-body
+ .card
+ .card-header Sidekiq running processes
+ .card-body
- if @sidekiq_processes.empty?
%h4.cred
%i.fa.fa-exclamation-triangle
@@ -41,5 +41,5 @@
- .panel.panel-default
+ .card
%iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 5a4ed1c3a2a..7f34357f147 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -6,32 +6,32 @@
- else
Your message here
-= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
+= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
- .form-group
- = f.label :message, class: 'control-label'
+ .form-group.row
+ = f.label :message, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_area :message, class: "form-control js-autosize",
required: true,
data: { preview_path: preview_admin_broadcast_messages_path }
- .form-group.js-toggle-colors-container
- .col-sm-10.col-sm-offset-2
+ .form-group.row.js-toggle-colors-container
+ .col-sm-10.offset-sm-2
= link_to 'Customize colors', '#', class: 'js-toggle-colors-link'
- .form-group.js-toggle-colors-container.hide
- = f.label :color, "Background Color", class: 'control-label'
+ .form-group.row.js-toggle-colors-container.toggle-colors.hide
+ = f.label :color, "Background Color", class: 'col-form-label col-sm-2'
.col-sm-10
= f.color_field :color, class: "form-control"
- .form-group.js-toggle-colors-container.hide
- = f.label :font, "Font Color", class: 'control-label'
+ .form-group.row.js-toggle-colors-container.toggle-colors.hide
+ = f.label :font, "Font Color", class: 'col-form-label col-sm-2'
.col-sm-10
= f.color_field :font, class: "form-control"
- .form-group
- = f.label :starts_at, class: 'control-label'
+ .form-group.row
+ = f.label :starts_at, _("Starts at (UTC)"), class: 'col-form-label col-sm-2'
.col-sm-10.datetime-controls
= f.datetime_select :starts_at, {}, class: 'form-control form-control-inline'
- .form-group
- = f.label :ends_at, class: 'control-label'
+ .form-group.row
+ = f.label :ends_at, _("Ends at (UTC)"), class: 'col-form-label col-sm-2'
.col-sm-10.datetime-controls
= f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
.form-actions
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index b806882eee3..9ef58faf8cc 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -32,7 +32,7 @@
%td
= message.ends_at
%td
- = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs'
- = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger'
+ = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-sm'
+ = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-sm btn-danger'
= paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/admin/conversational_development_index/_card.html.haml b/app/views/admin/conversational_development_index/_card.html.haml
index 6c8688e06ae..57eda06630b 100644
--- a/app/views/admin/conversational_development_index/_card.html.haml
+++ b/app/views/admin/conversational_development_index/_card.html.haml
@@ -3,20 +3,20 @@
.convdev-card-title
%h3
= card.title
- .text-light
+ .light-text
= card.description
- .card-scores
- .card-score
- .card-score-value
+ .board-card-scores
+ .board-card-score
+ .board-card-score-value
= format_score(card.instance_score)
- .card-score-name You
- .card-score
- .card-score-value
+ .board-card-score-name You
+ .board-card-score
+ .board-card-score-value
= format_score(card.leader_score)
- .card-score-name Lead
- .card-score-big
+ .board-card-score-name Lead
+ .board-card-score-big
= number_to_percentage(card.percentage_score, precision: 1)
- .card-buttons
+ .board-card-buttons
- if card.blog
%a{ href: card.blog }
= icon('info-circle', 'aria-hidden' => 'true')
diff --git a/app/views/admin/conversational_development_index/_disabled.html.haml b/app/views/admin/conversational_development_index/_disabled.html.haml
index 975d7df3da6..0a741b50960 100644
--- a/app/views/admin/conversational_development_index/_disabled.html.haml
+++ b/app/views/admin/conversational_development_index/_disabled.html.haml
@@ -1,5 +1,5 @@
.container.convdev-empty
- .col-sm-6.col-sm-push-3.text-center
+ .col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_index')
%h4 Usage ping is not enabled
%p
diff --git a/app/views/admin/conversational_development_index/_no_data.html.haml b/app/views/admin/conversational_development_index/_no_data.html.haml
index b23d2b5ec3a..d69c46194b4 100644
--- a/app/views/admin/conversational_development_index/_no_data.html.haml
+++ b/app/views/admin/conversational_development_index/_no_data.html.haml
@@ -1,5 +1,5 @@
.container.convdev-empty
- .col-sm-6.col-sm-push-3.text-center
+ .col-sm-12.justify-content-center.text-center
= custom_icon('convdev_no_data')
%h4 Data is still calculating...
%p
diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml
index ed40e7b4d00..e3d1aa31dc2 100644
--- a/app/views/admin/conversational_development_index/show.html.haml
+++ b/app/views/admin/conversational_development_index/show.html.haml
@@ -21,11 +21,11 @@
score
= link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev')
- .convdev-cards.card-container
+ .convdev-cards.board-card-container
- @metric.cards.each do |card|
= render 'card', card: card
- .convdev-steps.visible-lg
+ .convdev-steps.d-none.d-lg-block.d-xl-block
- @metric.idea_to_production_steps.each_with_index do |step, index|
.convdev-step{ class: "convdev-#{score_level(step.percentage_score)}-score" }
= custom_icon("i2p_step_#{index + 1}")
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index bbf0e0fb95c..3cdeb103bb8 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -2,6 +2,8 @@
- breadcrumb_title "Dashboard"
%div{ class: container_class }
+ = render_if_exists "admin/licenses/breakdown", license: @license
+
.admin-dashboard.prepend-top-default
.row
.col-sm-4
@@ -10,7 +12,7 @@
= link_to admin_projects_path do
%h3.text-center
Projects:
- = number_with_delimiter(Project.cached_count)
+ = approximate_count_with_delimiters(@counts, Project)
%hr
= link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -19,7 +21,8 @@
= link_to admin_users_path do
%h3.text-center
Users:
- = number_with_delimiter(User.count)
+ = approximate_count_with_delimiters(@counts, User)
+ = render_if_exists 'users_statistics'
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -28,7 +31,7 @@
= link_to admin_groups_path do
%h3.text-center
Groups:
- = number_with_delimiter(Group.count)
+ = approximate_count_with_delimiters(@counts, Group)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
@@ -38,35 +41,35 @@
%h4 Statistics
%p
Forks
- %span.light.pull-right
- = number_with_delimiter(ForkedProjectLink.count)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, ForkedProjectLink)
%p
Issues
- %span.light.pull-right
- = number_with_delimiter(Issue.count)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Issue)
%p
Merge Requests
- %span.light.pull-right
- = number_with_delimiter(MergeRequest.count)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, MergeRequest)
%p
Notes
- %span.light.pull-right
- = number_with_delimiter(Note.count)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Note)
%p
Snippets
- %span.light.pull-right
- = number_with_delimiter(Snippet.count)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Snippet)
%p
SSH Keys
- %span.light.pull-right
- = number_with_delimiter(Key.count)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Key)
%p
Milestones
- %span.light.pull-right
- = number_with_delimiter(Milestone.count)
+ %span.light.float-right
+ = approximate_count_with_delimiters(@counts, Milestone)
%p
Active Users
- %span.light.pull-right
+ %span.light.float-right
= number_with_delimiter(User.active.count)
.col-md-4
.info-well
@@ -75,44 +78,47 @@
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (allow_signup? ? "on" : "off") }
= sign_up
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon allow_signup?
- ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab.config.ldap.enabled
- gravatar = "Gravatar"
%p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") }
= gravatar
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon gravatar_enabled?
- omniauth = "OmniAuth"
%p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") }
= omniauth
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab.config.omniauth.enabled
- reply_email = "Reply by email"
%p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") }
= reply_email
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
+
+ = render_if_exists 'elastic_and_geo'
+
- container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
= container_reg
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon Gitlab.config.registry.enabled
- gitlab_pages = 'GitLab Pages'
- gitlab_pages_enabled = Gitlab.config.pages.enabled
%p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
= gitlab_pages
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon gitlab_pages_enabled
- gitlab_shared_runners = 'Shared Runners'
- gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled
%p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") }
= gitlab_shared_runners
- %span.light.pull-right
+ %span.light.float-right
= boolean_to_icon gitlab_shared_runners_enabled
.col-md-4
.info-well
@@ -120,41 +126,44 @@
%h4
Components
- if Gitlab::CurrentSettings.version_check_enabled
- .pull-right
+ .float-right
= version_status_badge
%p
GitLab
- %span.pull-right
+ %span.float-right
= Gitlab::VERSION
- = "(#{Gitlab::REVISION})"
+ = "(#{Gitlab.revision})"
%p
GitLab Shell
- %span.pull-right
+ %span.float-right
= Gitlab::Shell.new.version
%p
GitLab Workhorse
- %span.pull-right
+ %span.float-right
= gitlab_workhorse_version
%p
GitLab API
- %span.pull-right
+ %span.float-right
= API::API::version
- if Gitlab.config.pages.enabled
%p
GitLab Pages
- %span.pull-right
+ %span.float-right
= Gitlab::Pages::VERSION
+
+ = render_if_exists 'geo'
+
%p
Ruby
- %span.pull-right
+ %span.float-right
#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
%p
Rails
- %span.pull-right
+ %span.float-right
#{Rails::VERSION::STRING}
%p
= Gitlab::Database.adapter_name
- %span.pull-right
+ %span.float-right
= Gitlab::Database.version
%p
= link_to "Gitaly Servers", admin_gitaly_servers_path
@@ -166,7 +175,7 @@
- @projects.each do |project|
%p
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
- %span.light.pull-right
+ %span.light.float-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
.info-well
@@ -176,7 +185,7 @@
%p
= link_to [:admin, user], class: 'str-truncated-60' do
= user.name
- %span.light.pull-right
+ %span.light.float-right
#{time_ago_with_tooltip(user.created_at)}
.col-md-4
.info-well
@@ -186,5 +195,5 @@
%p
= link_to [:admin, group], class: 'str-truncated-60' do
= group.full_name
- %span.light.pull-right
+ %span.light.float-right
#{time_ago_with_tooltip(group.created_at)}
diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml
index 3a59282e578..b50adef362f 100644
--- a/app/views/admin/deploy_keys/edit.html.haml
+++ b/app/views/admin/deploy_keys/edit.html.haml
@@ -3,7 +3,7 @@
%hr
%div
- = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f|
+ = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
= f.submit 'Save changes', class: 'btn-save btn'
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 1420163fd5a..52ab8bae119 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -2,7 +2,7 @@
%h3.page-title.deploy-keys-title
Public deploy keys (#{@deploy_keys.count})
- .pull-right
+ .float-right
= link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
- if @deploy_keys.any?
@@ -29,6 +29,6 @@
%span.cgray
added #{time_ago_with_tooltip(deploy_key.created_at)}
%td
- .pull-right
+ .float-right
= link_to 'Edit', edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm'
= link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key'
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index 13f5259698f..d4f8e340b69 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -3,7 +3,7 @@
%hr
%div
- = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f|
+ = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
= f.submit 'Create', class: 'btn-create btn'
diff --git a/app/views/admin/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml
index 231f94dc95d..d0cf5761726 100644
--- a/app/views/admin/gitaly_servers/index.html.haml
+++ b/app/views/admin/gitaly_servers/index.html.haml
@@ -6,7 +6,7 @@
- if @gitaly_servers.any?
.table-holder
%table.table.responsive-table
- %thead.hidden-sm.hidden-xs
+ %thead.d-none.d-sm-none.d-md-block
%tr
%th= _("Storage")
%th= n_("Gitaly|Address")
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index d9f05003904..dc4dccc9e0d 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -1,23 +1,23 @@
-= form_for [:admin, @group], html: { class: "form-horizontal" } do |f|
+= form_for [:admin, @group] do |f|
= form_errors(@group)
= render 'shared/group_form', f: f
- .form-group.group-description-holder
- = f.label :avatar, "Group avatar", class: 'control-label'
+ .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/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f
= render 'groups/group_admin_settings', f: f
- if @group.new_record?
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
.alert.alert-info
= render 'shared/group_tips'
.form-actions
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 47cc2d4d27e..e7c70a6f187 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -5,7 +5,7 @@
= link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats
- %span.badge
+ %span.badge.badge-pill
= storage_counter(group.storage_size)
%span
@@ -20,7 +20,7 @@
= visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40
- = group_icon(group, class: "avatar s40 hidden-xs")
+ = group_icon(group, class: "avatar s40 d-none d-sm-block")
.title
= link_to [:admin, group], class: 'group-name' do
= group.full_name
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 324f3c0a22f..5ec612d0c72 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -4,14 +4,14 @@
%h3.page-title
Group: #{@group.full_name}
- = link_to admin_group_edit_path(@group), class: "btn pull-right" do
+ = link_to admin_group_edit_path(@group), class: "btn float-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
.row
.col-md-6
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Group info:
%ul.well-list
%li
@@ -58,46 +58,46 @@
= group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- .panel.panel-default
- .panel-heading
- %h3.panel-title
+ .card
+ .card-header
+ %h3.card-title
Projects
- %span.badge
+ %span.badge.badge-pill
#{@group.projects.count}
%ul.well-list
- @projects.each do |project|
%li
%strong
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
- %span.badge
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
- %span.pull-right.light
+ %span.float-right.light
%span.monospace= project.full_path + '.git'
- .panel-footer
+ .card-footer
= paginate @projects, param_name: 'projects_page', theme: 'gitlab'
- if @group.shared_projects.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Projects shared with #{@group.name}
- %span.badge
+ %span.badge.badge-pill
#{@group.shared_projects.count}
%ul.well-list
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
= link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
- %span.badge
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
- %span.pull-right.light
+ %span.float-right.light
%span.monospace= project.full_path + '.git'
.col-md-6
- if can?(current_user, :admin_group_member, @group)
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Add user(s) to the group:
- .panel-body.form-holder
+ .card-body.form-holder
%p.light
Read more about project permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
@@ -111,14 +111,14 @@
= button_tag 'Add users to group', class: "btn btn-create"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong= @group.name
group members
- %span.badge= @group.members.size
- .pull-right
- = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-xs"
+ %span.badge.badge-pill= @group.members.size
+ .float-right
+ = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm"
%ul.well-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
- .panel-footer
+ .card-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index e31fb58b205..d51ac854b04 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
%h3.page-title= page_title
.bs-callout.clearfix
- .pull-left
+ .float-left
%p
#{ s_('HealthCheck|Access token is') }
%code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
@@ -25,8 +25,8 @@
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
%hr
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Current Status:
- if no_errors
= icon('circle', class: 'cgreen')
@@ -34,7 +34,7 @@
- else
= icon('warning', class: 'cred')
#{ s_('HealthCheck|Unhealthy') }
- .panel-body
+ .card-body
- if no_errors
#{ s_('HealthCheck|No Health Problems Detected') }
- else
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
index 91a8c0c62fe..1d7c9930b6a 100644
--- a/app/views/admin/hook_logs/_index.html.haml
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -18,8 +18,8 @@
%tr
%td
= render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
- %td.hidden-xs
- %span.label.label-gray.deploy-project-label
+ %td.d-none.d-sm-block
+ %span.badge.badge-gray.deploy-project-label
= hook_log.trigger.singularize.titleize
%td
= truncate(hook_log.url, length: 50)
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
index 56127bacda2..2eb3ac85722 100644
--- a/app/views/admin/hook_logs/show.html.haml
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -4,7 +4,7 @@
%hr
-= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), class: "btn btn-default float-right prepend-left-10"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
index a6324a97fd5..e54dbd20ef4 100644
--- a/app/views/admin/hooks/_form.html.haml
+++ b/app/views/admin/hooks/_form.html.haml
@@ -6,39 +6,39 @@
.form-group
= form.label :token, 'Secret Token', class: 'label-light'
= form.text_field :token, class: 'form-control'
- %p.help-block
+ %p.form-text.text-muted
Use this token to validate received payloads
.form-group
= form.label :url, 'Trigger', class: 'label-light'
%ul.list-unstyled
%li
- .help-block
+ .form-text.text-muted
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
- = form.check_box :repository_update_events, class: 'pull-left'
+ = form.check_box :repository_update_events, class: 'float-left'
.prepend-left-20
= form.label :repository_update_events, class: 'list-label' do
%strong Repository update events
%p.light
This URL will be triggered when repository is updated
%li
- = form.check_box :push_events, class: 'pull-left'
+ = form.check_box :push_events, class: 'float-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered for each branch updated to the repository
%li
- = form.check_box :tag_push_events, class: 'pull-left'
+ = form.check_box :tag_push_events, class: 'float-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This URL will be triggered when a new tag is pushed to the repository
%li
- = form.check_box :merge_requests_events, class: 'pull-left'
+ = form.check_box :merge_requests_events, class: 'float-left'
.prepend-left-20
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge request events
@@ -46,7 +46,7 @@
This URL will be triggered when a merge request is created/updated/merged
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
- .checkbox
+ .form-check
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index 629b1a9940f..b9a650e1f1f 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -9,12 +9,12 @@
%hr
-= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
+= form_for @hook, as: :hook, url: admin_hook_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
= render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: @hook
- = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+ = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' }
%hr
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index d9e2ce5e74c..87f9b0e86a7 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -15,8 +15,8 @@
%hr
- if @hooks.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
System hooks (#{@hooks.count})
%ul.content-list
- @hooks.each do |hook|
@@ -29,7 +29,7 @@
%div
- SystemHook.triggers.each_value do |event|
- if hook.public_send(event)
- %span.label.label-gray= event.to_s.titleize
- %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
+ %span.badge.badge-gray= event.to_s.titleize
+ %span.badge.badge-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= render 'shared/plugins/index'
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 5381b854f5c..231c0f70882 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -1,13 +1,13 @@
-= form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f|
+= form_for [:admin, @user, @identity], html: { class: 'fieldset-form' } do |f|
= form_errors(@identity)
- .form-group
- = f.label :provider, class: 'control-label'
+ .form-group.row
+ = f.label :provider, class: 'col-form-label col-sm-2'
.col-sm-10
- values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control'
- .form-group
- = f.label :extern_uid, "Identifier", class: 'control-label'
+ .form-group.row
+ = f.label :extern_uid, "Identifier", class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :extern_uid, class: 'form-control', required: true
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index ef5a3f1d969..50fe9478a78 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -4,9 +4,9 @@
%td
= identity.extern_uid
%td
- = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-xs btn-grouped' do
+ = link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do
Edit
= link_to [:admin, @user, identity], method: :delete,
- class: 'btn btn-xs btn-danger',
+ class: 'btn btn-sm btn-danger',
data: { confirm: "Are you sure you want to remove this identity?" } do
Delete
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index ff67e59cdac..ee51fb3fda1 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -1,7 +1,7 @@
- page_title "Identities", @user.name, "Users"
= render 'admin/users/head'
-= link_to 'New identity', new_admin_user_identity_path, class: 'pull-right btn btn-new'
+= link_to 'New identity', new_admin_user_identity_path, class: 'float-right btn btn-new'
- if @identities.present?
.table-holder
%table.table
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index d5e6bede36a..ee51b44e83e 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -1,21 +1,22 @@
-= form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f|
+= form_for [:admin, @label], html: { class: 'label-form js-requires-input' } do |f|
= form_errors(@label)
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :title, class: "form-control", required: true
- .form-group
- = f.label :description, class: 'control-label'
+ .form-group.row
+ = f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :description, class: "form-control js-quick-submit"
- .form-group
- = f.label :color, "Background color", class: 'control-label'
+ .form-group.row
+ = f.label :color, "Background color", class: 'col-form-label col-sm-2'
.col-sm-10
.input-group
- .input-group-addon.label-color-preview &nbsp;
+ .input-group-prepend
+ .input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control"
- .help-block
+ .form-text.text-muted
Choose any color.
%br
Or you can choose one of suggested colors below
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 77b174fbb27..009a47dd517 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -2,6 +2,6 @@
.label-row
= render_colored_label(label, tooltip: false)
= markdown_field(label, :description)
- .pull-right
+ .float-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
= link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml
index 9d51762890f..7a0dcbdd1c6 100644
--- a/app/views/admin/labels/destroy.js.haml
+++ b/app/views/admin/labels/destroy.js.haml
@@ -1,2 +1,2 @@
- if @labels.size == 0
- $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000)
+ $('.labels').load(document.URL + ' .card.bg-light').hide().fadeIn(1000)
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index 05d6b9ed238..add38fb333e 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,7 +1,7 @@
- page_title "Labels"
%div
- = link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
+ = link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do
New label
%h3.page-title
Labels
@@ -13,6 +13,6 @@
= render @labels
= paginate @labels, theme: 'gitlab'
- else
- .light-well
+ .card.bg-light
.nothing-here-block There are no labels yet
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 78757b6384f..a6c436cd1f4 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -2,7 +2,7 @@
- page_title "Logs"
%div{ class: container_class }
- %ul.nav-links.log-tabs
+ %ul.nav-links.log-tabs.nav.nav-tabs
- @loggers.each do |klass|
%li{ class: active_when(klass == @loggers.first) }>
= link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }
@@ -15,7 +15,7 @@
.js-file-title.file-title
%i.fa.fa-file
= klass.file_name
- .pull-right
+ .float-right
= link_to '#', class: 'log-bottom' do
%i.fa.fa-arrow-down
Scroll down
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index b5d7b889504..00933d726d9 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -12,10 +12,10 @@
= s_('AdminProjects|Delete')
.stats
- %span.badge
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
- if project.archived
- %span.label.label-warning archived
+ %span.badge.badge-warning archived
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index c37d8ac45b9..57de792f92d 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -15,7 +15,7 @@
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.full_path}"
= dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
- .dropdown-menu.dropdown-select.dropdown-menu-align-right
+ .dropdown-menu.dropdown-select.dropdown-menu-right
= dropdown_title('Namespaces')
= dropdown_filter("Search for Namespace")
= dropdown_content
@@ -25,7 +25,7 @@
New Project
= button_tag "Search", class: "btn btn-primary btn-search hide"
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
= link_to admin_projects_path do
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index aeba9788fda..29ec712b6b7 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -3,24 +3,24 @@
- page_title @project.full_name, "Projects"
%h3.page-title
Project: #{@project.full_name}
- = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
+ = link_to edit_project_path(@project), class: "btn btn-nr float-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
- if @project.last_repository_check_failed?
.row
.col-md-12
- .panel
- .panel-heading.alert.alert-danger
+ .card
+ .card-header.alert.alert-danger
Last repository check
- = "(#{time_ago_in_words(@project.last_repository_check_at)} ago)"
+ = "(#{time_ago_with_tooltip(@project.last_repository_check_at)})"
failed. See
= link_to 'repocheck.log', admin_logs_path
for error messages.
.row
.col-md-6
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Project info:
%ul.well-list
%li
@@ -110,13 +110,13 @@
= visibility_level_icon(@project.visibility_level)
= visibility_level_label(@project.visibility_level)
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Transfer project
- .panel-body
- = form_for @project, url: transfer_admin_project_path(@project), method: :put, html: { class: 'form-horizontal' } do |f|
- .form-group
- = f.label :new_namespace_id, "Namespace", class: 'control-label'
+ .card-body
+ = form_for @project, url: transfer_admin_project_path(@project), method: :put do |f|
+ .form-group.row
+ = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-2'
.col-sm-10
.dropdown
= dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
@@ -126,14 +126,14 @@
= dropdown_content
= dropdown_loading
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= f.submit 'Transfer', class: 'btn btn-primary'
- .panel.panel-default.repository-check
- .panel-heading
+ .card.repository-check
+ .card-header
Repository check
- .panel-body
+ .card-body
= form_for @project, url: repository_check_admin_project_path(@project), method: :post do |f|
.form-group
- if @project.last_repository_check_at.nil?
@@ -158,29 +158,29 @@
.col-md-6
- if @group
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong= @group.name
group members
- %span.badge= @group_members.size
- .pull-right
- = link_to admin_group_path(@group), class: 'btn btn-xs' do
+ %span.badge.badge-pill= @group_members.size
+ .float-right
+ = link_to admin_group_path(@group), class: 'btn btn-sm' do
= icon('pencil-square-o', text: 'Manage access')
%ul.well-list.content-list.members-list
= render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
- .panel-footer
+ .card-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters, force_mobile_view: true
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong= @project.name
project members
- %span.badge= @project.users.size
- .pull-right
- = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-xs"
+ %span.badge.badge-pill= @project.users.size
+ .float-right
+ = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm"
%ul.well-list.project_members.content-list.members-list
= render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
- .panel-footer
+ .card-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
index cb02a750490..adfc67d66d0 100644
--- a/app/views/admin/requests_profiles/index.html.haml
+++ b/app/views/admin/requests_profiles/index.html.haml
@@ -13,8 +13,8 @@
- if @profiles.present?
.prepend-top-default
- @profiles.each do |path, profiles|
- .panel.panel-default.panel-small
- .panel-heading
+ .card.card-small
+ .card-header
%code= path
%ul.content-list
- profiles.each do |profile|
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index e1cee584929..a6cd39edcf0 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -1,13 +1,15 @@
%tr{ id: dom_id(runner) }
%td
- if runner.shared?
- %span.label.label-success shared
+ %span.badge.badge-success shared
+ - elsif runner.group_type?
+ %span.badge.badge-success group
- else
- %span.label.label-info specific
+ %span.badge.badge-info specific
- if runner.locked?
- %span.label.label-warning locked
+ %span.badge.badge-warning locked
- unless runner.active?
- %span.label.label-danger paused
+ %span.badge.badge-danger paused
%td
= link_to admin_runner_path(runner) do
@@ -19,7 +21,7 @@
%td
= runner.ip_address
%td
- - if runner.shared?
+ - if runner.shared? || runner.group_type?
n/a
- else
= runner.projects.count(:all)
@@ -27,15 +29,15 @@
#{runner.builds.count(:all)}
%td
- runner.tag_list.sort.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
%td
- if runner.contacted_at
- #{time_ago_in_words(runner.contacted_at)} ago
+ = time_ago_with_tooltip runner.contacted_at
- else
Never
%td.admin-runner-btn-group-cell
- .pull-right.btn-group
+ .float-right.btn-group
= link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do
= icon('pencil')
&nbsp;
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 9f13dbbbd82..f38aeb151df 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -14,20 +14,23 @@
%span Each Runner can be in one of the following states:
%ul
%li
- %span.label.label-success shared
+ %span.badge.badge-success shared
\- Runner runs jobs from all unassigned projects
%li
- %span.label.label-info specific
+ %span.badge.badge-success group
+ \- Runner runs jobs from all unassigned projects in its group
+ %li
+ %span.badge.badge-info specific
\- Runner runs jobs from assigned projects
%li
- %span.label.label-warning locked
+ %span.badge.badge-warning locked
\- Runner cannot be assigned to other projects
%li
- %span.label.label-danger paused
+ %span.badge.badge-danger paused
\- Runner will not receive any new jobs
.bs-callout.clearfix
- .pull-left
+ .float-left
%p
You can reset runners registration token by pressing a button below.
.prepend-top-10
@@ -39,13 +42,13 @@
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token }
.append-bottom-20.clearfix
- .pull-left
+ .float-left
= form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
.form-group
- = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
+ = search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false
= submit_tag 'Search', class: 'btn'
- .pull-right.light
+ .float-right.light
Runners with last contact more than a minute ago: #{@active_runners_cnt}
%br
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 37269862de6..8a0c2bf4c5f 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -1,7 +1,7 @@
= content_for :title do
%h3.project-title
Runner ##{@runner.id}
- .pull-right
+ .float-right
- if @runner.shared?
%span.runner-state.runner-state-shared
Shared
@@ -19,6 +19,9 @@
%p
If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
+- elsif @runner.group_type?
+ .bs-callout.bs-callout-success
+ %h4 This runner will process jobs from all projects in its group and subgroups
- else
.bs-callout.bs-callout-info
%h4 This Runner will process jobs only from ASSIGNED projects
@@ -26,7 +29,7 @@
%hr
.append-bottom-20
- = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner)
+ = render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner)
.row
.col-md-6
@@ -45,8 +48,8 @@
%strong
= project.full_name
%td
- .pull-right
- = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
+ .float-right
+ = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-sm'
%table.table.unassigned-projects
%thead
@@ -67,10 +70,10 @@
%td
= project.full_name
%td
- .pull-right
+ .float-right
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: @runner.id
- = f.submit 'Enable', class: 'btn btn-xs'
+ = f.submit 'Enable', class: 'btn btn-sm'
= paginate @projects, theme: "gitlab"
.col-md-6
@@ -108,4 +111,4 @@
%td.timestamp
- if build.finished_at
- %span #{time_ago_in_words build.finished_at} ago
+ %span= time_ago_with_tooltip build.finished_at
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index e5b8ebdf613..144dceacbdd 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -3,7 +3,7 @@
%p #{@service.description} template
-= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form|
+= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form|
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index 50132572096..89872c1b91a 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -20,5 +20,4 @@
%td
= service.description
%td.light
- = time_ago_in_words service.updated_at
- ago
+ = time_ago_with_tooltip service.updated_at
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index ea6a0c4fb77..9d47dc1cce5 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -24,16 +24,16 @@
%td
- if user
= link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
- data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
+ data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-sm btn-remove"
%td
- if spam_log.submitted_as_ham?
- .btn.btn-xs.disabled
+ .btn.btn-sm.disabled
Submitted as ham
- else
- = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning'
+ = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-sm btn-warning'
- if user && !user.blocked?
- = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm"
- else
- .btn.btn-xs.disabled
+ .btn.btn-sm.disabled
Already blocked
- = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
+ = link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-sm btn-close js-remove-tr"
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 23f9927cfee..b19934e028d 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -5,7 +5,7 @@
.prepend-top-default
.row
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 CPU
.data
- if @cpus
@@ -14,7 +14,7 @@
= icon('warning', class: 'text-warning')
Unable to collect CPU info
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 Memory Usage
.data
- if @memory
@@ -23,7 +23,7 @@
= icon('warning', class: 'text-warning')
Unable to collect memory info
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 Disk Usage
.data
- @disks.each do |disk|
@@ -31,7 +31,7 @@
%p= disk[:disk_name]
%p= disk[:mount_path]
.col-sm-4
- .light-well
+ .card.bg-light.light-well
%h4 Uptime
.data
%h1= distance_of_time_in_words_to_now(Rails.application.config.booted_at)
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 794aaec89bd..35a331283ab 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -1,15 +1,15 @@
%fieldset
%legend Access
.form-group
- = f.label :projects_limit, class: 'control-label'
+ = f.label :projects_limit, class: 'col-form-label'
.col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group
- = f.label :can_create_group, class: 'control-label'
+ = f.label :can_create_group, class: 'col-form-label'
.col-sm-10= f.check_box :can_create_group
.form-group
- = f.label :access_level, class: 'control-label'
+ = f.label :access_level, class: 'col-form-label'
.col-sm-10
- editing_current_user = (current_user == @user)
@@ -29,7 +29,7 @@
You cannot remove your own admin rights.
.form-group
- = f.label :external, class: 'control-label'
+ = f.label :external, class: 'col-form-label'
.col-sm-10
= f.check_box :external do
External
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index e911af3f6f9..010cb2ac354 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -1,21 +1,21 @@
.user_new
- = form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
= form_errors(@user)
%fieldset
%legend Account
- .form-group
- = f.label :name, class: 'control-label'
+ .form-group.row
+ = f.label :name, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
- .form-group
- = f.label :username, class: 'control-label'
+ .form-group.row
+ = f.label :username, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control'
%span.help-inline * required
- .form-group
- = f.label :email, class: 'control-label'
+ .form-group.row
+ = f.label :email, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
@@ -23,8 +23,8 @@
- if @user.new_record?
%fieldset
%legend Password
- .form-group
- = f.label :password, class: 'control-label'
+ .form-group.row
+ = f.label :password, class: 'col-form-label col-sm-2'
.col-sm-10
%strong
Reset link will be generated and sent to the user.
@@ -33,33 +33,33 @@
- else
%fieldset
%legend Password
- .form-group
- = f.label :password, class: 'control-label'
+ .form-group.row
+ = f.label :password, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control'
- .form-group
- = f.label :password_confirmation, class: 'control-label'
+ .form-group.row
+ = f.label :password_confirmation, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
= render partial: 'access_levels', locals: { f: f }
%fieldset
%legend Profile
- .form-group
- = f.label :avatar, class: 'control-label'
+ .form-group.row
+ = f.label :avatar, class: 'col-form-label col-sm-2'
.col-sm-10
= f.file_field :avatar
- .form-group
- = f.label :skype, class: 'control-label'
+ .form-group.row
+ = f.label :skype, class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :skype, class: 'form-control'
- .form-group
- = f.label :linkedin, class: 'control-label'
+ .form-group.row
+ = f.label :linkedin, class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :linkedin, class: 'form-control'
- .form-group
- = f.label :twitter, class: 'control-label'
+ .form-group.row
+ = f.label :twitter, class: 'col-form-label'
.col-sm-10= f.text_field :twitter, class: 'form-control'
- .form-group
- = f.label :website_url, 'Website', class: 'control-label'
+ .form-group.row
+ = f.label :website_url, 'Website', class: 'col-form-label col-sm-2'
.col-sm-10= f.text_field :website_url, class: 'form-control'
.form-actions
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index be41c33b853..bfbc16d37a0 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -7,14 +7,14 @@
- if @user.admin
%span.cred (Admin)
- .pull-right
+ .float-right
- if @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
%hr
-%ul.nav-links
+%ul.nav-links.nav.nav-tabs
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#projects') do
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 6bc217f84cc..af22652e07c 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -1,5 +1,5 @@
-.panel.panel-default
- .panel-heading
+.card
+ .card-header
Profile
%ul.well-list
%li
diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml
index a126a858ea8..81cfb71af16 100644
--- a/app/views/admin/users/_projects.html.haml
+++ b/app/views/admin/users/_projects.html.haml
@@ -1,13 +1,13 @@
- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present?
- .panel.panel-default.contributed-projects
- .panel-heading Projects contributed to
+ .card.contributed-projects
+ .card-header Projects contributed to
= render 'shared/projects/list',
projects: contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 5, stars: true, avatar: false
- if local_assigns.has_key?(:projects) && projects.present?
- .panel.panel-default
- .panel-heading Personal projects
+ .card
+ .card-header Personal projects
= render 'shared/projects/list',
projects: projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: false
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 2ff4221efbd..5e176b61d68 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -5,11 +5,11 @@
.user-name.row-title.str-truncated-100
= link_to user.name, [:admin, user]
- if user.blocked?
- %span.label.label-danger blocked
+ %span.badge.badge-danger blocked
- if user.admin?
- %span.label.label-success Admin
+ %span.badge.badge-success Admin
- if user.external?
- %span.label.label-default External
+ %span.badge.badge-secondary External
- if user == current_user
%span It's you!
.row-second-line.str-truncated-100
@@ -21,7 +21,7 @@
%a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } }
= icon('cog')
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
Settings
%li
@@ -38,19 +38,19 @@
%li.divider
- if user.can_be_removed?
%li
- %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user),
username: user.name,
- delete_contributions: 'false' }, type: 'button' }
+ delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user')
%li
- %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
username: user.name,
- delete_contributions: 'true' }, type: 'button' }
+ delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 0ef4b71f4fe..faeb82656ba 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -13,7 +13,7 @@
.dropdown
- toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li.dropdown-header
Sort by
%li
@@ -38,35 +38,35 @@
= icon('angle-left')
.fade-right
= icon('angle-right')
- %ul.nav-links.scrolling-tabs
+ %ul.nav-links.nav.nav-tabs.scrolling-tabs
= nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do
Active
- %small.badge= number_with_delimiter(User.active.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.active)
= nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do
Admins
- %small.badge= number_with_delimiter(User.admins.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.admins)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled
- %small.badge= number_with_delimiter(User.with_two_factor.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.with_two_factor)
= nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled
- %small.badge= number_with_delimiter(User.without_two_factor.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_two_factor)
= nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do
External
- %small.badge= number_with_delimiter(User.external.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.external)
= nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do
Blocked
- %small.badge= number_with_delimiter(User.blocked.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
Without projects
- %small.badge= number_with_delimiter(User.without_projects.count)
+ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects)
%ul.flex-list.content-list
- if @users.empty?
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 96835ee9af5..469a7bd9715 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -2,19 +2,19 @@
= render 'admin/users/head'
- if @user.groups.any?
- .panel.panel-default
- .panel-heading Group projects
- %ul.well-list
+ .card
+ .card-header Group projects
+ %ul.card-body-list
- @user.group_members.includes(:source).each do |group_member|
- group = group_member.group
%li.group_member
%strong= link_to group.name, admin_group_path(group)
&ndash; access to
#{pluralize(group.projects.count, 'project')}
- .pull-right
+ .float-right
%span.light.vertical-align-middle= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from group' do
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-sm btn btn-remove prepend-left-10", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
.row
@@ -26,9 +26,9 @@
.col-md-6
- .panel.panel-default
- .panel-heading Joined projects (#{@joined_projects.count})
- %ul.well-list
+ .card
+ .card-header Joined projects (#{@joined_projects.count})
+ %ul.card-body-list
- @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
@@ -37,12 +37,12 @@
= project.full_name
- if member
- .pull-right
+ .float-right
- if member.owner?
%span.light Owner
- else
%span.light.vertical-align-middle= member.human_access
- if member.respond_to? :project
- = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove prepend-left-10", title: 'Remove user from project' do
+ = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-sm btn btn-remove prepend-left-10", title: 'Remove user from project' do
%i.fa.fa-times
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index ec3be869797..a74fcea65d8 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -5,8 +5,8 @@
.row
.col-md-6
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
= @user.name
%ul.well-list
%li
@@ -18,8 +18,8 @@
= @user.username
= render 'admin/users/profile', user: @user
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Account:
%ul.well-list
%li
@@ -37,7 +37,7 @@
%li
%span.light Secondary email:
%strong= email.email
- = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
+ = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-sm btn btn-remove float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
%i.fa.fa-times
%li.two-factor-status
@@ -45,7 +45,7 @@
%strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- if @user.two_factor_enabled?
Enabled
- = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-xs btn-remove pull-right', title: 'Disable Two-factor Authentication'
+ = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn btn-sm btn-remove float-right', title: 'Disable Two-factor Authentication'
- else
Disabled
@@ -128,20 +128,20 @@
.col-md-6
- unless @user == current_user
- unless @user.confirmed?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
Confirm user
- .panel-body
+ .card-body
- if @user.unconfirmed_email.present?
- email = " (#{@user.unconfirmed_email})"
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
%br
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- if @user.blocked?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
This user is blocked
- .panel-body
+ .card-body
%p A blocked user cannot:
%ul
%li Log in
@@ -149,10 +149,10 @@
%br
= link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- else
- .panel.panel-warning
- .panel-heading
+ .card.bg-warning
+ .card-header
Block this user
- .panel-body
+ .card-body
%p Blocking user has the following effects:
%ul
%li User will not be able to login
@@ -162,28 +162,28 @@
%br
= link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning"
- if @user.access_locked?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
This account has been locked
- .panel-body
+ .card-body
%p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.
%br
= link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- .panel.panel-danger
- .panel-heading
+ .card.bg-danger
+ .card-header
= s_('AdminUsers|Delete user')
- .panel-body
+ .card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%br
- %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: @user.name,
- delete_contributions: 'false' }, type: 'button' }
+ delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user')
- else
- if @user.solo_owned_groups.present?
@@ -196,10 +196,10 @@
%p
You don't have access to delete this user.
- .panel.panel-danger
- .panel-heading
+ .card.bg-danger
+ .card-header
= s_('AdminUsers|Delete user and contributions')
- .panel-body
+ .card-body
- if can?(current_user, :destroy_user, @user)
%p
This option deletes the user and any contributions that
@@ -210,12 +210,12 @@
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%br
- %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
+ %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user),
username: @user.name,
- delete_contributions: 'true' }, type: 'button' }
+ delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
- else
%p
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 22f149d1caa..d4455749803 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,8 +1,8 @@
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/feature_moved.svg'
- .col-xs-12
+ .col-12
.text-content.text-center
%h4= _("GitLab CI Linter has been moved")
%p
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index db2040110fa..8b0463db000 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -6,15 +6,15 @@
- tooltip = "#{subject.name} - #{status.status_tooltip}"
- if status.has_details?
- = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: true, container: 'body' } do
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, html: 'true', container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
- else
- .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' } }
+ .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' } }
%span{ class: klass }= sprite_icon(status.icon)
%span.ci-build-text= subject.name
- if status.has_action?
- = link_to status.action_path, class: "ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
+ = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 440623b34f5..571eb28f195 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -17,14 +17,14 @@
.ci-variable-row-body
%input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id }
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
- %input.js-ci-variable-input-key.ci-variable-body-item.form-control{ type: "text",
+ %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control{ type: "text",
name: key_input_name,
value: key,
placeholder: s_('CiVariables|Input variable key') }
.ci-variable-body-item
- .form-control.js-secret-value-placeholder{ class: ('hide' unless id) }
+ .form-control.js-secret-value-placeholder.qa-ci-variable-input-value{ class: ('hide' unless id) }
= '*' * 20
- %textarea.js-ci-variable-input-value.js-secret-value.form-control{ class: ('hide' if id),
+ %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ class: ('hide' if id),
rows: 1,
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 7a3f3667ac1..7503548fa3d 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
%li{ class: active_when(params[:filter].nil?) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 617c20b9635..d8f1e50544c 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links.mobile-separator
+ %ul.nav-links.mobile-separator.nav.nav-tabs
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: _("Your groups") do
Your groups
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 449a2ce625e..9b1d9b659f9 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -4,7 +4,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs.mobile-separator
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index a9488df07bd..e7e323a8683 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your snippets
@@ -8,5 +8,5 @@
Explore snippets
- if current_user
- .nav-controls.hidden-xs
+ .nav-controls.d-none.d-sm-block
= link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml
index 97f854cc5f0..da3cf5807b0 100644
--- a/app/views/dashboard/projects/_nav.html.haml
+++ b/app/views/dashboard/projects/_nav.html.haml
@@ -1,5 +1,5 @@
.nav-block
- %ul.nav-links.mobile-separator
+ %ul.nav-links.mobile-separator.nav.nav-tabs
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index b1efe59aadc..8933d9e31ff 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -11,5 +11,5 @@
- if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
- else
- %h3 You don't have starred projects yet
+ %h3.page-title You don't have starred projects yet
%p.slead Visit project page and press on star icon and it will appear on this page.
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index e86b1ab3116..4391624196b 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -5,7 +5,7 @@
= render 'dashboard/snippets_head'
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
-.visible-xs
+.d-block.d-sm-none
&nbsp;
= link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
New snippet
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 664966989db..d5a9cc646a6 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -4,18 +4,18 @@
- if current_user.todos.any?
.top-area
- %ul.nav-links.mobile-separator
+ %ul.nav-links.mobile-separator.nav.nav-tabs
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
Todos
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(todos_pending_count)
%li.todos-done{ class: active_when(params[:state] == 'done') }>
= link_to todos_filter_path(state: 'done') do
%span
Done
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(todos_done_count)
.nav-controls
@@ -35,7 +35,7 @@
- if params[:project_id].present?
= hidden_field_tag(:project_id, params[:project_id])
= dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
+ placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } })
.filter-item.inline
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
@@ -60,7 +60,7 @@
- else
= sort_title_recently_created
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-sort.dropdown-menu-right
%li
= link_to todos_filter_path(sort: sort_value_label_priority) do
= sort_title_label_priority
@@ -73,7 +73,7 @@
- if @todos.any?
.js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
- .panel.panel-default.panel-without-border.panel-without-margin
+ .card.card-without-border.card-without-margin
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
index 79e3a35cc9a..8ddfd3ea74a 100644
--- a/app/views/devise/mailer/unlock_instructions.html.haml
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -2,7 +2,7 @@
= email_default_heading("Hello, #{@resource.name}!")
%p
Your GitLab account has been locked due to an excessive amount of unsuccessful
- sign in attempts. Your account will automatically unlock in #{time_ago_in_words(Devise.unlock_in.from_now)}
+ sign in attempts. Your account will automatically unlock in #{distance_of_time_in_words(Devise.unlock_in)}
or you may click the link below to unlock now.
#cta
= link_to('Unlock account', unlock_url(@resource, unlock_token: @token))
diff --git a/app/views/devise/mailer/unlock_instructions.text.erb b/app/views/devise/mailer/unlock_instructions.text.erb
index 3aea3e20145..8d4abbf3500 100644
--- a/app/views/devise/mailer/unlock_instructions.text.erb
+++ b/app/views/devise/mailer/unlock_instructions.text.erb
@@ -1,7 +1,7 @@
Hello, <%= @resource.name %>!
Your GitLab account has been locked due to an excessive amount of unsuccessful
-sign in attempts. Your account will automatically unlock in <%= time_ago_in_words(Devise.unlock_in.from_now) %>
+sign in attempts. Your account will automatically unlock in <%= distance_of_time_in_words(Devise.unlock_in) %>
or you may click the link below to unlock now.
<%= unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 41462f503cb..c45d2214592 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -6,11 +6,11 @@
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- if devise_mapping.rememberable?
- .remember-me.checkbox
+ .remember-me
%label{ for: "user_remember_me" }
= f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
- .pull-right.forgot-password
+ .float-right.forgot-password
= link_to "Forgot your password?", new_password_path(:user)
.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 2556cb6f59b..36ff42090be 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -6,7 +6,7 @@
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
- .remember-me.checkbox
+ .remember-me
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 3159d21598a..6bf7349f602 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -6,7 +6,7 @@
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable?
- .remember-me.checkbox
+ .remember-me
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 6e54b9b5645..ba168c4eab8 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -9,7 +9,7 @@
%div
= f.label 'Two-Factor Authentication code', name: :otp_attempt
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
- %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
+ %p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-save"
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 546cec4d565..3723814debe 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -7,7 +7,7 @@
%span.light
- has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}"
- %fieldset.prepend-top-10.checkbox.remember-me
+ %fieldset.prepend-top-10.remember-me
%label
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
%span
diff --git a/app/views/devise/shared/_tab_single.html.haml b/app/views/devise/shared/_tab_single.html.haml
index 7bd414d64c3..5683b4207b4 100644
--- a/app/views/devise/shared/_tab_single.html.haml
+++ b/app/views/devise/shared/_tab_single.html.haml
@@ -1,3 +1,3 @@
-%ul.nav-links.new-session-tabs.single-tab
- %li.active
- %a= tab_title
+%ul.nav-links.new-session-tabs.single-tab.nav-tabs.nav
+ %li.nav-item
+ %a.nav-link.active= tab_title
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index f50e0724e09..087af61235b 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,13 +1,13 @@
-%ul.nav-links.new-session-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) }
+%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
- %li.active
- = link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
- %li{ class: active_when(i.zero? && !crowd_enabled?) }
- = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
+ %li.nav-item{ class: active_when(i.zero? && !crowd_enabled?) }
+ = link_to server['label'], "##{server['provider_name']}", class: 'nav-link', 'data-toggle' => 'tab'
- if password_authentication_enabled_for_web?
- %li
- = link_to 'Standard', '#login-pane', 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to 'Standard', '#login-pane', class: 'nav-link', 'data-toggle' => 'tab'
- if allow_signup?
- %li
- = link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
+ %li.nav-item
+ = link_to 'Register', '#register-pane', class: 'nav-link', 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index fa3c3df7f60..284d4fa1b89 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,6 +1,6 @@
-%ul.nav-links.new-session-tabs{ role: 'tablist' }
- %li.active{ role: 'presentation' }
- %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
+%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in
- if allow_signup?
- %li{ role: 'presentation' }
- %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
+ %li.nav-item{ role: 'presentation' }
+ %a.nav-link{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index e9589213f80..1765251c93d 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -13,7 +13,7 @@
= icon("chevron-up")
- else
= icon("chevron-down")
- Toggle discussion
+ = _('Toggle discussion')
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
@@ -51,5 +51,5 @@
- if discussion.diff_discussion? && discussion.diff_file
= render "discussions/diff_with_notes", discussion: discussion
- else
- .panel.panel-default
+ .card
= render partial: "discussions/notes", locals: { discussion: discussion, disable_collapse_class: true }
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index 1cc227428e9..30b00ca86b3 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -11,7 +11,7 @@
- if discussion.try(:on_image?) && show_toggle
%button.diff-notes-collapse.js-diff-notes-toggle{ type: 'button' }
= sprite_icon('collapse', css_class: 'collapse-icon')
- %button.btn-transparent.badge.js-diff-notes-toggle{ type: 'button' }
+ %button.btn-transparent.badge.badge-pill.js-diff-notes-toggle{ type: 'button' }
= badge_counter
= render partial: "shared/notes/note", collection: discussion.notes, as: :note, locals: { badge_counter: badge_counter, show_image_comment_badge: show_image_comment_badge }
@@ -22,7 +22,7 @@
- if discussion.potentially_resolvable?
- line_type = local_assigns.fetch(:line_type, nil)
- .btn-group-justified.discussion-with-resolve-btn{ role: "group" }
+ .btn-group.discussion-with-resolve-btn{ role: "group" }
.btn-group{ role: "group" }
= link_to_reply_discussion(discussion, line_type)
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index cf0e0de1ca4..be0935b8313 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -9,10 +9,10 @@
= f.label :redirect_uri, class: 'label-light'
= f.text_area :redirect_uri, class: 'form-control', required: true
- %span.help-block
+ %span.form-text.text-muted
Use one line per URI
- if Doorkeeper.configuration.native_redirect_uri
- %span.help-block
+ %span.form-text.text-muted
Use
%code= Doorkeeper.configuration.native_redirect_uri
for local tests
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index d1237d7bf6f..cdf3ff81bd9 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -73,7 +73,7 @@
%tr
%td
Anonymous
- .help-block
+ .form-text.text-muted
%em Authorization was granted by entering your username and password in the application.
%td= token.created_at
%td= token.scopes
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 6364f0be4a3..89ad626f73f 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -30,5 +30,5 @@
= render "shared/tokens/scopes_list", token: @application
.form-actions
- = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
+ = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide float-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/doorkeeper/authorized_applications/index.html.haml b/app/views/doorkeeper/authorized_applications/index.html.haml
index c8a585560a2..30c9d02b72e 100644
--- a/app/views/doorkeeper/authorized_applications/index.html.haml
+++ b/app/views/doorkeeper/authorized_applications/index.html.haml
@@ -1,4 +1,4 @@
-%header.page-header
+%header
%h1 Your authorized applications
%main{ :role => "main" }
.table-holder
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index e3c5fd55f08..bc1d32607e4 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -6,7 +6,7 @@
at
= event.created_at.to_s(:short)
- unless event.rm_ref?
- %blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author)
+ .blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author)
- if event.commits_count > 1
%p
%i
diff --git a/app/views/explore/groups/_nav.html.haml b/app/views/explore/groups/_nav.html.haml
index c8d95b52156..ab4787c6d05 100644
--- a/app/views/explore/groups/_nav.html.haml
+++ b/app/views/explore/groups/_nav.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path do
Explore Groups
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index efa8b2706da..0643b9cfbc5 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -9,7 +9,7 @@
= render 'nav'
- if cookies[:explore_groups_landing_dismissed] != 'true'
- .explore-groups.landing.content-block.js-explore-groups-landing.hidden
+ .explore-groups.landing.content-block.js-explore-groups-landing.hide
%button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times')
.svg-container
= custom_icon('icon_explore_groups_splash')
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index f630f1effdc..845f4046d0d 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -8,7 +8,7 @@
- else
Any
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
= link_to filter_projects_path(visibility_level: nil) do
Any
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index e0a2a1e9c96..558cd26f1e0 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -1,5 +1,5 @@
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
= nav_link(page: [trending_explore_projects_path, explore_root_path]) do
= link_to trending_explore_projects_path do
Trending
diff --git a/app/views/groups/_create_chat_team.html.haml b/app/views/groups/_create_chat_team.html.haml
index 20de1b4c973..9a3ff0313b5 100644
--- a/app/views/groups/_create_chat_team.html.haml
+++ b/app/views/groups/_create_chat_team.html.haml
@@ -1,10 +1,10 @@
.form-group
- = f.label :create_chat_team, class: 'control-label' do
+ = f.label :create_chat_team, class: 'col-form-label' do
%span.mattermost-icon
= custom_icon('icon_mattermost')
Mattermost
.col-sm-10
- .checkbox.js-toggle-container
+ .form-check.js-toggle-container
= f.label :create_chat_team do
.js-toggle-button= f.check_box(:create_chat_team, { checked: true }, true, false)
Create a Mattermost team for this group
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index 2ace1e2dd1e..3cd3fb32b9c 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -1,28 +1,26 @@
-- if current_user.admin?
- .form-group
- = f.label :lfs_enabled, 'Large File Storage', class: 'control-label'
- .col-sm-10
- .checkbox
- = f.label :lfs_enabled do
- = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
- %strong
- Allow projects within this group to use Git LFS
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- %br/
- %span.descr This setting can be overridden in each project.
+.form-group.row
+ = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.label :lfs_enabled do
+ = f.check_box :lfs_enabled, checked: @group.lfs_enabled?
+ %strong
+ Allow projects within this group to use Git LFS
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ %br/
+ %span.descr This setting can be overridden in each project.
-- if can? current_user, :admin_group, @group
- .form-group
- = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2'
- .col-sm-10
- .checkbox
- = f.label :require_two_factor_authentication do
- = f.check_box :require_two_factor_authentication
- %strong
- Require all users in this group to setup Two-factor authentication
- = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.text_field :two_factor_grace_period, class: 'form-control'
- .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+.form-group.row
+ = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.label :require_two_factor_authentication do
+ = f.check_box :require_two_factor_authentication
+ %strong
+ Require all users in this group to setup Two-factor authentication
+ = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group')
+.form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
+ = f.text_field :two_factor_grace_period, class: 'form-control'
+ .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 86cd0759a2c..96ed63937fa 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,14 +1,16 @@
- breadcrumb_title "General Settings"
-.panel.panel-default.prepend-top-default
- .panel-heading
+- @content_class = "limit-container-width" unless fluid_layout
+
+.card.prepend-top-default
+ .card-header
Group settings
- .panel-body
- = form_for @group, html: { multipart: true, class: "form-horizontal gl-show-field-errors" }, authenticity_token: true do |f|
+ .card-body
+ = form_for @group, html: { multipart: true, class: "gl-show-field-errors" }, authenticity_token: true do |f|
= form_errors(@group)
= render 'shared/group_form', f: f
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
.avatar-container.s160
= group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
@@ -23,15 +25,15 @@
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f
- .form-group
- %label.control-label
+ .form-group.row
+ %label.col-form-label.col-sm-2
= s_("GroupSettings|Share with group lock")
.col-sm-10
- .checkbox
+ .form-check
= f.label :share_with_group_lock do
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
@@ -45,9 +47,9 @@
.form-actions
= f.submit 'Save group', class: "btn btn-save"
-.panel.panel-danger
- .panel-heading Remove group
- .panel-body
+.card.bg-danger
+ .card-header Remove group
+ .card-body
= form_tag(@group, method: :delete) do
%p
Removing group will cause all child projects and resources to be removed.
@@ -58,9 +60,9 @@
= button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
- if supports_nested_groups?
- .panel.panel-warning
- .panel-heading Transfer group
- .panel-body
+ .card.bg-warning
+ .card-header Transfer group
+ .card-body
= form_for @group, url: transfer_group_path(@group), method: :put do |f|
.form-group
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } })
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 5b1a4630c56..aa03f8365f9 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -2,12 +2,12 @@
.row
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
@@ -15,7 +15,7 @@
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index ad9d5562ded..6a0321bcd2b 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,10 +1,11 @@
- page_title "Members"
+- can_manage_members = can?(current_user, :admin_group_member, @group)
.project-members-page.prepend-top-default
%h4
Members
%hr
- - if can?(current_user, :admin_group_member, @group)
+ - if can_manage_members
.project-members-new.append-bottom-default
%p.clearfix
Add new member to
@@ -13,20 +14,23 @@
= render 'shared/members/requests', membership_source: @group, requesters: @requesters
- .append-bottom-default.clearfix
+ .clearfix
%h5.member.existing-title
Existing members
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
- %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
- = icon("search")
- = render 'shared/members/sort_dropdown'
- .panel.panel-default
- .panel-heading
- Members with access to
- %strong= @group.name
+ .card
+ .card-header.flex-project-members-panel
+ %span.flex-project-title
+ Members with access to
+ %strong= @group.name
%span.badge= @members.total_count
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
+ %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
+ = icon("search")
+ - if can_manage_members
+ = render 'shared/members/filter_2fa_dropdown'
+ = render 'shared/members/sort_dropdown'
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index bbfbea4ac7a..662db18cf86 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -8,7 +8,7 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn' do
+ = link_to safe_params.merge(rss_url_options), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index a1be0d3220a..6d35457a0ec 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -1,14 +1,14 @@
-= form_for [@group, @milestone], html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
+= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
.row
= form_errors(@milestone)
.col-md-6
- .form-group
- = f.label :title, "Title", class: "control-label"
+ .form-group.row
+ = f.label :title, "Title", class: "col-form-label col-sm-2"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
- .form-group.milestone-description
- = f.label :description, "Description", class: "control-label"
+ .form-group.row.milestone-description
+ = f.label :description, "Description", class: "col-form-label col-sm-2"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index e9daac95ca1..1e72d88db1e 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -8,12 +8,12 @@
New Group
%hr
-= form_for @group, html: { class: 'group-form form-horizontal gl-show-field-errors' } do |f|
+= 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.group-description-holder
- = f.label :avatar, "Group avatar", class: 'control-label'
+ .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
@@ -21,8 +21,8 @@
= render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= render 'shared/group_tips'
.form-actions
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index ef181b425bc..cd07b95155c 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title "Projects"
-.panel.panel-default.prepend-top-default
- .panel-heading
+.card.prepend-top-default
+ .card-header
%strong= @group.name
projects:
- if can? current_user, :admin_group, @group
@@ -15,10 +15,10 @@
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
%strong= link_to project.full_name, project
- .pull-right
+ .float-right
- if project.archived
- %span.label.label-warning archived
- %span.badge
+ %span.badge.badge-warning archived
+ %span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
= link_to 'Members', project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml
new file mode 100644
index 00000000000..e6c089c3494
--- /dev/null
+++ b/app/views/groups/runners/_group_runners.html.haml
@@ -0,0 +1,24 @@
+- link = link_to _('Runners API'), help_page_path('api/runners.md')
+
+%h3
+ = _('Group Runners')
+
+.bs-callout.bs-callout-warning
+ = _('GitLab Group Runners can execute code for all the projects in this group.')
+ = _('They can be managed using the %{link}.').html_safe % { link: link }
+
+-# Proper policies should be implemented per
+-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
+- if can?(current_user, :admin_pipeline, @group)
+ = render partial: 'ci/runner/how_to_setup_runner',
+ locals: { registration_token: @group.runners_token, type: 'group' }
+
+- if @group.runners.empty?
+ %h4.underlined-title
+ = _('This group does not provide any group Runners yet.')
+
+- else
+ %h4.underlined-title
+ = _('Available group Runners : %{runners}.').html_safe % { runners: @group.runners.count }
+ %ul.bordered-list
+ = render partial: 'groups/runners/runner', collection: @group.runners, as: :runner
diff --git a/app/views/groups/runners/_index.html.haml b/app/views/groups/runners/_index.html.haml
new file mode 100644
index 00000000000..0cf9011b471
--- /dev/null
+++ b/app/views/groups/runners/_index.html.haml
@@ -0,0 +1,9 @@
+= render 'shared/runners/runner_description'
+
+%hr
+
+%p.lead
+ = _('To start serving your jobs you can add Runners to your group')
+.row
+ .col-sm-6
+ = render 'groups/runners/group_runners'
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
new file mode 100644
index 00000000000..76650a961d6
--- /dev/null
+++ b/app/views/groups/runners/_runner.html.haml
@@ -0,0 +1,27 @@
+%li.runner{ id: dom_id(runner) }
+ %h4
+ = runner_status_icon(runner)
+
+ = link_to runner.short_sha, group_runner_path(@group, runner), class: 'commit-sha'
+
+ %small.edit-runner
+ = link_to edit_group_runner_path(@group, runner) do
+ = icon('edit')
+
+ .pull-right
+ - if runner.active?
+ = link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
+ - else
+ = link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm'
+ = link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
+ .pull-right
+ %small.light
+ \##{runner.id}
+ - if runner.description.present?
+ %p.runner-description
+ = runner.description
+ - if runner.tag_list.present?
+ %p
+ - runner.tag_list.sort.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/groups/runners/edit.html.haml b/app/views/groups/runners/edit.html.haml
new file mode 100644
index 00000000000..fcd096eeaa0
--- /dev/null
+++ b/app/views/groups/runners/edit.html.haml
@@ -0,0 +1,6 @@
+- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", 'Runners'
+
+%h4 Runner ##{@runner.id}
+
+%hr
+ = render 'shared/runners/form', runner: @runner, runner_form_url: group_runner_path(@group, @runner)
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index dd82922ec55..082e1b7befa 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -1,11 +1,27 @@
- breadcrumb_title "CI / CD Settings"
- page_title "CI / CD"
-%h4
- = _('Secret variables')
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+- expanded = Rails.env.test?
-%p
- = render "ci/variables/content"
+%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Secret variables')
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
+ %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ = expanded ? _('Collapse') : _('Expand')
+ %p.append-bottom-0
+ = render "ci/variables/content"
+ .settings-content
+ = render 'ci/variables/index', save_endpoint: group_variables_path
-= render 'ci/variables/index', save_endpoint: group_variables_path
+%section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Runners settings')
+ %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Register and see your runners for this group.')
+ .settings-content
+ = render 'groups/runners/index'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 8e1dea4afc1..8b0ef3cd87a 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -20,7 +20,7 @@
%input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } }
%button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } }
= icon("caret-down", class: "dropdown-btn-icon")
- %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
+ %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
%li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } }
.menu-item
.icon-container
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 29b23ae2e52..9a3a03a7671 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -1,5 +1,5 @@
#modal-shortcuts.modal{ tabindex: -1 }
- .modal-dialog
+ .modal-dialog.modal-lg
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
@@ -121,7 +121,7 @@
%tr
%td.shortcut
.key g
- .key e
+ .key v
%td
Go to the project's activity feed
%tr
@@ -175,6 +175,18 @@
%tr
%td.shortcut
.key g
+ .key e
+ %td
+ Go to environments
+ %tr
+ %td.shortcut
+ .key g
+ .key k
+ %td
+ Go to kubernetes
+ %tr
+ %td.shortcut
+ .key g
.key s
%td
Go to snippets
@@ -219,6 +231,17 @@
%td.shortcut
.key y
%td Go to file permalink
+ %tbody
+ %tr
+ %th
+ %th Web IDE
+ %tr
+ %td.shortcut
+ - if browser.platform.mac?
+ .key &#8984; p
+ - else
+ .key ctrl p
+ %td Go to file
.col-lg-4
%table.shortcut-mappings
%tbody.hidden-shortcut.network{ style: 'display:none' }
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index bf2725dc328..6391a13dd25 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -8,7 +8,7 @@
Community Edition
- if user_signed_in?
%span= Gitlab::VERSION
- %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
+ %small= link_to Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab.revision)
= version_status_badge
%hr
@@ -33,8 +33,8 @@
.documentation-index.wiki
= markdown(@help_index)
.col-md-4
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Quick help
%ul.well-list
%li= link_to 'See our website for getting help', support_url
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index ce09b44fbb2..94b012d39a3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -74,10 +74,10 @@
= lorem
.cover-controls
- = link_to '#', class: 'btn btn-gray' do
+ = link_to '#', class: 'btn btn-default' do
= icon('pencil')
&nbsp;
- = link_to '#', class: 'btn btn-gray' do
+ = link_to '#', class: 'btn btn-default' do
= icon('rss')
%h2#lists Lists
@@ -129,8 +129,8 @@
.lead
List inside panel
.example
- .panel.panel-default
- .panel-heading Your list
+ .card
+ .card-header Your list
%ul.well-list
%li
One item
@@ -174,7 +174,7 @@
.example
.top-area
- %ul.nav-links
+ %ul.nav-links.nav.nav-tabs
%li.active
%a Open
%li
@@ -205,8 +205,7 @@
%h2#buttons Buttons
.example
- %button.btn.btn-default{ :type => "button" } Default
- %button.btn.btn-gray{ :type => "button" } Gray
+ %button.btn.btn-default{ :type => "button" } Secondary
%button.btn.btn-primary{ :type => "button" } Primary
%button.btn.btn-success{ :type => "button" } Success
%button.btn.btn-info{ :type => "button" } Info
@@ -218,7 +217,7 @@
.example
.clearfix
- .dropdown.inline.pull-left
+ .dropdown.inline.float-left
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
@@ -226,11 +225,11 @@
%li
%a{ href: "#" }
Dropdown option
- .dropdown.inline.pull-right
+ .dropdown.inline.float-right
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
Dropdown
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
%a{ href: "#" }
Dropdown option
@@ -416,26 +415,26 @@
.row
.col-md-6
- .panel.panel-success
- .panel-heading Success
- .panel-body
+ .card.bg-success
+ .card-header Success
+ .card-body
= lorem
- .panel.panel-primary
- .panel-heading Primary
- .panel-body
+ .card.bg-primary
+ .card-header Primary
+ .card-body
= lorem
- .panel.panel-info
- .panel-heading Info
- .panel-body
+ .card.bg-info
+ .card-header Info
+ .card-body
= lorem
.col-md-6
- .panel.panel-warning
- .panel-heading Warning
- .panel-body
+ .card.bg-warning
+ .card-header Warning
+ .card-body
= lorem
- .panel.panel-danger
- .panel-heading Danger
- .panel-body
+ .card.bg-danger
+ .card-header Danger
+ .card-body
= lorem
%h2#alerts Alerts
@@ -461,23 +460,23 @@
%code form.horizontal-form
.example
- %form.form-horizontal
- .form-group
- %label.col-sm-2.control-label{ :for => "inputEmail3" } Email
+ %form
+ .form-group.row
+ %label.col-sm-2.col-form-label{ :for => "inputEmail3" } Email
.col-sm-10
%input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/
- .form-group
- %label.col-sm-2.control-label{ :for => "inputPassword3" } Password
+ .form-group.row
+ %label.col-sm-2.col-form-label{ :for => "inputPassword3" } Password
.col-sm-10
%input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
%label
%input{ :type => "checkbox" }/
Remember me
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
%button.btn.btn-default{ :type => "submit" } Sign in
.lead
@@ -492,7 +491,7 @@
.form-group
%label{ :for => "exampleInputPassword1" } Password
%input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/
- .checkbox
+ .form-check
%label
%input{ :type => "checkbox" }/
Remember me
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index e0e8fe548d0..da9331b45dd 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -1,9 +1,6 @@
- @body_class = 'ide'
- page_title 'IDE'
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'ide', force_same_domain: true
-
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } }
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index 638c8b5a672..f7094375023 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -46,14 +46,15 @@
%td.import-target
%fieldset.row
.input-group
- .project-path.input-group-btn
+ .project-path.input-group-prepend
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
- else
- = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
- %span.input-group-addon /
+ = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-prepend
+ .input-group-text /
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 9589e0956f4..4e8f715db4f 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -54,14 +54,15 @@
%td.import-target
%fieldset.row
.input-group
- .project-path.input-group-btn
+ .project-path.input-group-prepend
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
- else
- = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
- %span.input-group-addon /
+ = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
+ %span.input-group-prepend
+ .input-group-text /
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: 'btn btn-import js-add-to-import' do
@@ -73,7 +74,7 @@
= link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
- = label_tag 'Incompatible Project', nil, class: 'label label-danger'
+ = label_tag 'Incompatible Project', nil, class: 'label badge-danger'
- if @incompatible_repos.any?
%p
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index 5515fad6f48..d5a37095365 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -5,21 +5,21 @@
Import projects from FogBugz
%hr
-= form_tag callback_import_fogbugz_path, class: 'form-horizontal' do
+= form_tag callback_import_fogbugz_path do
%p
To get started you enter your FogBugz URL and login information below.
In the next steps, you'll be able to map users and select the projects
you want to import.
- .form-group
- = label_tag :uri, 'FogBugz URL', class: 'control-label'
+ .form-group.row
+ = label_tag :uri, 'FogBugz URL', class: 'col-form-label col-sm-8'
.col-sm-4
= text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control'
- .form-group
- = label_tag :email, 'FogBugz Email', class: 'control-label'
+ .form-group.row
+ = label_tag :email, 'FogBugz Email', class: 'col-form-label col-sm-8'
.col-sm-4
= text_field_tag :email, nil, class: 'form-control'
- .form-group
- = label_tag :password, 'FogBugz Password', class: 'control-label'
+ .form-group.row
+ = label_tag :password, 'FogBugz Password', class: 'col-form-label col-sm-8'
.col-sm-4
= password_field_tag :password, nil, class: 'form-control'
.form-actions
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 84e0009487f..d27c5d3c36d 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -5,7 +5,7 @@
Import projects from FogBugz
%hr
-= form_tag create_user_map_import_fogbugz_path, class: 'form-horizontal' do
+= form_tag create_user_map_import_fogbugz_path do
%p
Customize how FogBugz email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import.
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index 02a116f996b..eb9790c7903 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -10,13 +10,13 @@
= succeed '.' do
= link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token'
-= form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do
- .form-group
- = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label'
+= form_tag personal_access_token_import_gitea_path do
+ .form-group.row
+ = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-8'
.col-sm-4
= text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
- .form-group
- = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label'
+ .form-group.row
+ = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-8'
.col-sm-4
= text_field_tag :personal_access_token, nil, class: 'form-control'
.form-actions
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index dec85368d10..2d059e78490 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -8,20 +8,22 @@
= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
.row
- .form-group.col-xs-12.col-sm-6
+ .form-group.col-12.col-sm-6
= label_tag :namespace_id, 'Project path', class: 'label-light'
.form-group
.input-group
- if current_user.can_select_namespace?
- .input-group-addon.has-tooltip{ title: root_url }
- = root_url
+ .input-group-prepend.has-tooltip{ title: root_url }
+ .input-group-text
+ = root_url
= select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1
- else
- .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- #{user_url(current_user.username)}/
+ .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
+ .input-group-text
+ #{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
- .form-group.col-xs-12.col-sm-6.project-path
+ .form-group.col-12.col-sm-6.project-path
= label_tag :path, 'Project name', class: 'label-light'
= text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index c5800a1cca0..2f1fb8d9c56 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -5,7 +5,7 @@
Import projects from Google Code
%hr
-= form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do
+= form_tag callback_import_google_code_path, multipart: true do
%p
Follow the steps below to export your Google Code project data.
In the next step, you'll be able to select the projects you want to import.
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
index 0738b3db1eb..91c774f575c 100644
--- a/app/views/import/google_code/new_user_map.html.haml
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -5,7 +5,7 @@
Import projects from Google Code
%hr
-= form_tag create_user_map_import_google_code_path, class: 'form-horizontal' do
+= form_tag create_user_map_import_google_code_path do
%p
Customize how Google Code email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import.
@@ -36,7 +36,7 @@
will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com.
By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address.
- .form-group
+ .form-group.row
.col-sm-12
= text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index bc61aeece72..acf7a108cb0 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -66,7 +66,7 @@
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
- = label_tag "Incompatible Project", nil, class: "label label-danger"
+ = label_tag "Incompatible Project", nil, class: "label badge-danger"
- if @incompatible_repos.any?
%p
diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml
index e7a70e3bb28..369165da02a 100644
--- a/app/views/kaminari/gitlab/_first_page.html.haml
+++ b/app/views/kaminari/gitlab/_first_page.html.haml
@@ -5,5 +5,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.first
- = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote
+%li.first.page-item
+ = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml
index 889514c4755..6eec30212d1 100644
--- a/app/views/kaminari/gitlab/_gap.html.haml
+++ b/app/views/kaminari/gitlab/_gap.html.haml
@@ -4,6 +4,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li
- %span.gap
- = raw(t 'views.pagination.truncate')
+%li.page-item.disabled
+ = link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml
index 53f780d1d1b..8b49db58281 100644
--- a/app/views/kaminari/gitlab/_last_page.html.haml
+++ b/app/views/kaminari/gitlab/_last_page.html.haml
@@ -5,5 +5,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.last
- = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote}
+%li.last.page-item
+ = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote, class: 'page-link'}
diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml
index c93dc7a50e8..05f151555ad 100644
--- a/app/views/kaminari/gitlab/_next_page.html.haml
+++ b/app/views/kaminari/gitlab/_next_page.html.haml
@@ -5,9 +5,8 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-- if current_page.last?
- %li.next.disabled
- %span= raw(t 'views.pagination.next')
-- else
- %li.next
- = link_to raw(t 'views.pagination.next'), url, rel: 'next', remote: remote
+
+- page_url = current_page.last? ? '#' : url
+
+%li.page-item{ class: ('disabled' if current_page.last?) }
+ = link_to raw(t 'views.pagination.next'), page_url, rel: 'next', remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 5c5be03a7cd..8a40e13a537 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li.page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] }
- = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil }
+%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] }
+ = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' }
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index 8fe6bd653ae..a6435deb4bf 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -7,7 +7,7 @@
-# paginator: the paginator that renders the pagination tags inside
= paginator.render do
.gl-pagination
- %ul.pagination.clearfix
+ %ul.pagination.justify-content-center
- unless current_page.first?
= first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages
= prev_page_tag
diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml
index b7c6caf7ff4..f4a11a449b7 100644
--- a/app/views/kaminari/gitlab/_prev_page.html.haml
+++ b/app/views/kaminari/gitlab/_prev_page.html.haml
@@ -5,9 +5,8 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-- if current_page.first?
- %li.prev.disabled
- %span= raw(t 'views.pagination.previous')
-- else
- %li.prev
- = link_to raw(t 'views.pagination.previous'), url, rel: 'prev', remote: remote
+
+- page_url = current_page.first? ? '#' : url
+
+%li.page-item{ class: ('disabled' if current_page.first?) }
+ = link_to raw(t 'views.pagination.previous'), page_url, rel: 'prev', remote: remote, class: 'page-link'
diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml
index 250029c4475..1425a809052 100644
--- a/app/views/kaminari/gitlab/_without_count.html.haml
+++ b/app/views/kaminari/gitlab/_without_count.html.haml
@@ -1,8 +1,8 @@
.gl-pagination
%ul.pagination.clearfix
- if previous_path
- %li.prev
- = link_to(t('views.pagination.previous'), previous_path, rel: 'prev')
+ %li.page-item.prev
+ = link_to(t('views.pagination.previous'), previous_path, rel: 'prev', class: 'page-link')
- if next_path
- %li.next
- = link_to(t('views.pagination.next'), next_path, rel: 'next')
+ %li.page-item.next
+ = link_to(t('views.pagination.next'), next_path, rel: 'next', class: 'page-link')
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 05ddd0ec733..8bd5708d490 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,10 @@
+- extra_flash_class = local_assigns.fetch(:extra_flash_class, nil)
+
.flash-container.flash-container-page
-# We currently only support `alert`, `notice`, `success`
- flash.each do |key, value|
-# Don't show a flash message if the message is nil
- if value
%div{ class: "flash-#{key}" }
- %div{ class: (container_class) }
+ %div{ class: "#{container_class} #{extra_flash_class}" }
%span= value
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index b981b5fdafa..02bdfe9aa3c 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -38,9 +38,6 @@
= yield :library_javascripts
= javascript_include_tag locale_path unless I18n.locale == :en
- = webpack_bundle_tag "webpack_runtime"
- = webpack_bundle_tag "common"
- = webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
- if content_for?(:page_specific_javascripts)
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 4276e6ee4bb..240e03a5d53 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,16 +1,11 @@
-- project = @target_project || @project
+- object = @target_project || @project || @group
- noteable_type = @noteable.class if @noteable.present?
-- if project
+- datasources = autocomplete_data_sources(object, noteable_type)
+
+- if object
-# haml-lint:disable InlineJavaScript
:javascript
gl = window.gl || {};
gl.GfmAutoComplete = gl.GfmAutoComplete || {};
- gl.GfmAutoComplete.dataSources = {
- members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
- issues: "#{issues_project_autocomplete_sources_path(project)}",
- mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}",
- labels: "#{labels_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
- milestones: "#{milestones_project_autocomplete_sources_path(project)}",
- commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}"
- };
+ gl.GfmAutoComplete.dataSources = #{datasources.to_json};
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 52587760ba4..91f796adb2e 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -7,7 +7,7 @@
- if @project && @project.persisted?
- project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: project_issues_path(@project), mr_path: project_merge_requests_path(@project), issues_disabled: !@project.issues_enabled? }
.search.search-form{ class: "#{'has-location-badge' if label.present?}" }
- = form_tag search_path, method: :get, class: 'navbar-form' do |f|
+ = form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
- if label.present?
.location-badge= label
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 6513b719199..82ca7252424 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -10,9 +10,7 @@
.content
= render "layouts/flash"
.row
- .col-sm-5.pull-right.new-session-forms-container
- = yield
- .col-sm-7.brand-holder.pull-left
+ .col-sm-7.brand-holder
%h1
= brand_title
= brand_image
@@ -28,6 +26,8 @@
- if Gitlab::CurrentSettings.sign_in_text.present?
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text)
+ .col-sm-5.new-session-forms-container
+ = yield
%hr.footer-fixed
.container.footer-container
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
new file mode 100644
index 00000000000..24b6c490a5a
--- /dev/null
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -0,0 +1,22 @@
+- return unless current_user
+
+%ul
+ %li.current-user
+ .user-name.bold
+ = current_user.name
+ = current_user.to_reference
+ %li.divider
+ - if current_user_menu?(:profile)
+ %li
+ = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
+ - if current_user_menu?(:settings)
+ %li
+ = link_to s_("CurrentUser|Settings"), profile_path
+ - if current_user_menu?(:help)
+ %li
+ = link_to _("Help"), help_path
+ - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
+ %li.divider
+ - if current_user_menu?(:sign_out)
+ %li
+ = link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index e6238c0dddb..1bca837a311 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-gitlab.qa-navbar
+%header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
@@ -8,7 +8,7 @@
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
- %span.logo-text.hidden-xs
+ %span.logo-text.d-none.d-sm-block
= logo_text
- if current_user
@@ -21,9 +21,9 @@
- if current_user
= render 'layouts/header/new_dropdown'
- if header_link?(:search)
- %li.hidden-sm.hidden-xs
+ %li.nav-item.d-none.d-sm-none.d-md-block
= render 'layouts/search' unless current_controller?(:search)
- %li.visible-sm-inline-block.visible-xs-inline-block
+ %li.nav-item.d-inline-block.d-sm-none.d-md-none
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('search', size: 16)
@@ -32,53 +32,38 @@
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
- %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
+ %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('git-merge', size: 16)
- merge_requests_count = assigned_issuables_count(:merge_requests)
- %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
+ %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
- if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('todo-done', size: 16)
- %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
+ %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
- if header_link?(:user_dropdown)
- %li.header-user.dropdown
+ %li.nav-item.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu-nav.dropdown-menu-align-right
- %ul
- %li.current-user
- .user-name.bold
- = current_user.name
- @#{current_user.username}
- %li.divider
- %li
- = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
- %li
- = link_to "Settings", profile_path
- - if current_user
- %li
- = link_to "Help", help_path
- %li.divider
- %li
- = link_to "Sign out", destroy_user_session_path, class: "sign-out-link"
+ .dropdown-menu.dropdown-menu-right
+ = render 'layouts/header/current_user_dropdown'
- if header_link?(:admin_impersonation)
- %li.impersonation
- = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ %li.nav-item.impersonation
+ = 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
+ %li.nav-item
%div
- = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
+ = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'nav-link btn btn-sign-in'
- %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
+ %button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only Toggle navigation
= sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
diff --git a/app/views/layouts/header/_empty.html.haml b/app/views/layouts/header/_empty.html.haml
index 2ed4edb1136..2dfc787b7a8 100644
--- a/app/views/layouts/header/_empty.html.haml
+++ b/app/views/layouts/header/_empty.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-fixed-top.navbar-empty
+%header.navbar.fixed-top.navbar-empty
.container
- .center-logo
+ .mx-auto
= brand_header_logo
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 6f53f5ac1ae..db8137cc248 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -2,7 +2,7 @@
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
= sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu-nav.dropdown-menu-align-right
+ .dropdown-menu.dropdown-menu-right
%ul
- if @group&.persisted?
- create_group_project = can?(current_user, :create_projects, @group)
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index f773bd0832d..7647e25e804 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -8,34 +8,34 @@
= render "layouts/nav/projects_dropdown/show"
- if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-none d-sm-block" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do
Groups
- if dashboard_nav_link?(:activity)
- = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
+ = nav_link(path: 'dashboard#activity', html_options: { class: "d-none d-lg-block d-xl-block" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity
- if dashboard_nav_link?(:milestones)
- = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
+ = nav_link(controller: 'dashboard/milestones', html_options: { class: "d-none d-lg-block d-xl-block" }) do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
Milestones
- if dashboard_nav_link?(:snippets)
- = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
+ = nav_link(controller: 'dashboard/snippets', html_options: { class: "d-none d-lg-block d-xl-block" }) do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
- %li.header-more.dropdown.hidden-lg
+ %li.header-more.dropdown.d-lg-none.d-xl-none
%a{ href: "#", data: { toggle: "dropdown" } }
More
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu
%ul
- if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-block d-sm-none" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups
@@ -61,13 +61,13 @@
Projects
- if current_controller?('ide')
- %li.line-separator.hidden-xs
+ %li.line-separator.d-none.d-sm-block
= nav_link(controller: 'ide') do
= link_to '#', class: 'dashboard-shortcuts-web-ide', title: 'Web IDE' do
Web IDE
- if current_user.admin? || Gitlab::Sherlock.enabled?
- %li.line-separator.hidden-xs
+ %li.line-separator.d-none.d-sm-block
- if current_user.admin?
= nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index dd086f70641..62402c32e08 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -127,13 +127,13 @@
= sprite_icon('slight-frown')
%span.nav-item-name
Abuse Reports
- %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
+ %span.badge.badge-pill.count= number_with_delimiter(AbuseReport.count(:all))
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_abuse_reports_path do
%strong.fly-out-top-item-name
#{ _('Abuse Reports') }
- %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 517d9aa3d99..39a033337ff 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -43,14 +43,14 @@
= sprite_icon('issues')
%span.nav-item-name
Issues
- %span.badge.count= number_with_delimiter(issues_count)
+ %span.badge.badge-pill.count= number_with_delimiter(issues_count)
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
= link_to issues_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Issues') }
- %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
+ %span.badge.badge-pill.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
@@ -83,13 +83,13 @@
= sprite_icon('git-merge')
%span.nav-item-name
Merge Requests
- %span.badge.count= number_with_delimiter(merge_requests_count)
+ %span.badge.badge-pill.count= number_with_delimiter(merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
= link_to merge_requests_group_path(@group) do
%strong.fly-out-top-item-name
#{ _('Merge Requests') }
- %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
- if group_sidebar_link?(:group_members)
= nav_link(path: 'group_members#index') do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index c878fcf2808..6cbd163dd41 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -129,6 +129,17 @@
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
#{ _('Preferences') }
+ = nav_link(controller: :active_sessions) do
+ = link_to profile_active_sessions_path do
+ .nav-icon-container
+ = sprite_icon('monitor-lines')
+ %span.nav-item-name
+ Active Sessions
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_active_sessions_path do
+ %strong.fly-out-top-item-name
+ #{ _('Active Sessions') }
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 196db08cebd..fdb07ce6fc5 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -13,13 +13,13 @@
.nav-icon-container
= sprite_icon('project')
%span.nav-item-name
- Project
+ = _('Project')
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Overview') }
+ = _('Project')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
@@ -40,45 +40,45 @@
.nav-icon-container
= sprite_icon('doc_text')
%span.nav-item-name
- Repository
+ = _('Repository')
%ul.sidebar-sub-level-items
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Repository') }
+ = _('Repository')
%li.divider.fly-out-top-item
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do
- #{ _('Files') }
+ = _('Files')
= nav_link(controller: [:commit, :commits]) do
= link_to project_commits_path(@project, current_ref) do
- #{ _('Commits') }
+ = _('Commits')
= nav_link(html_options: {class: branches_tab_class}) do
= link_to project_branches_path(@project) do
- #{ _('Branches') }
+ = _('Branches')
= nav_link(controller: [:tags, :releases]) do
= link_to project_tags_path(@project) do
- #{ _('Tags') }
+ = _('Tags')
= nav_link(path: 'graphs#show') do
= link_to project_graph_path(@project, current_ref) do
- #{ _('Contributors') }
+ = _('Contributors')
= nav_link(controller: %w(network)) do
= link_to project_network_path(@project, current_ref) do
- #{ s_('ProjectNetworkGraph|Graph') }
+ = _('Graph')
= nav_link(controller: :compare) do
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
- #{ _('Compare') }
+ = _('Compare')
= nav_link(path: 'graphs#charts') do
= link_to charts_project_graph_path(@project, current_ref) do
- #{ _('Charts') }
+ = _('Charts')
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
@@ -86,24 +86,24 @@
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name
- Issues
+ = _('Issues')
- if @project.issues_enabled?
- %span.badge.count.issue_counter
- = number_with_delimiter(@project.open_issues_count)
+ %span.badge.badge-pill.count.issue_counter
+ = number_with_delimiter(@project.open_issues_count(current_user))
%ul.sidebar-sub-level-items
= nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Issues') }
+ = _('Issues')
- if @project.issues_enabled?
- %span.badge.count.issue_counter.fly-out-badge
- = number_with_delimiter(@project.open_issues_count)
+ %span.badge.badge-pill.count.issue_counter.fly-out-badge
+ = number_with_delimiter(@project.open_issues_count(current_user))
%li.divider.fly-out-top-item
= nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do
%span
- List
+ = _('List')
= nav_link(controller: :boards) do
= link_to project_boards_path(@project), title: boards_link_text do
@@ -113,12 +113,12 @@
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do
%span
- Labels
+ = _('Labels')
= nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: 'Milestones' do
%span
- Milestones
+ = _('Milestones')
- if project_nav_tab? :external_issue_tracker
= nav_link do
- issue_tracker = @project.external_issue_tracker
@@ -139,59 +139,80 @@
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name
- Merge Requests
- %span.badge.count.merge_counter.js-merge-counter
+ = _('Merge Requests')
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Merge Requests') }
- %span.badge.count.merge_counter.js-merge-counter.fly-out-badge
+ = _('Merge Requests')
+ %span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count)
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp]) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
- = sprite_icon('pipeline')
+ = sprite_icon('rocket')
%span.nav-item-name
- CI / CD
+ = _('CI / CD')
%ul.sidebar-sub-level-items
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" } ) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
- #{ _('CI / CD') }
+ = _('CI / CD')
%li.divider.fly-out-top-item
- if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
- Pipelines
+ = _('Pipelines')
- if project_nav_tab? :builds
= nav_link(controller: [:jobs, :artifacts]) do
= link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
- Jobs
+ = _('Jobs')
- if project_nav_tab? :pipelines
= nav_link(controller: :pipeline_schedules) do
= link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do
%span
- Schedules
+ = _('Schedules')
+
+ - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
+ = nav_link(path: 'pipelines#charts') do
+ = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
+ %span
+ = _('Charts')
+
+ - if project_nav_tab? :operations
+ = nav_link(controller: [:environments, :clusters, :user, :gcp]) do
+ = link_to project_environments_path(@project), class: 'shortcuts-operations' do
+ .nav-icon-container
+ = sprite_icon('cloud-gear')
+ %span.nav-item-name
+ = _('Operations')
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_environments_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Operations')
+ %li.divider.fly-out-top-item
- if project_nav_tab? :environments
= nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
- Environments
+ = _('Environments')
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
- = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do
+ = link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span
= _('Kubernetes')
- if show_cluster_hint
@@ -213,23 +234,22 @@
= link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
- %button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' }
+ %button.btn.btn-create.btn-sm.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it!")
= sprite_icon('thumb-up')
- - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
- = nav_link(path: 'pipelines#charts') do
- = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do
- %span
- Charts
-
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
= sprite_icon('disk')
%span.nav-item-name
- Registry
+ = _('Registry')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: %w[projects/registry/repositories], html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_container_registry_index_path(@project) do
+ %strong.fly-out-top-item-name
+ = _('Registry')
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
@@ -237,12 +257,12 @@
.nav-icon-container
= sprite_icon('book')
%span.nav-item-name
- Wiki
+ = _('Wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
= link_to get_project_wiki_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Wiki') }
+ = _('Wiki')
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
@@ -250,12 +270,12 @@
.nav-icon-container
= sprite_icon('snippet')
%span.nav-item-name
- Snippets
+ = _('Snippets')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Snippets') }
+ = _('Snippets')
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show]) do
@@ -263,7 +283,7 @@
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name.qa-settings-item
- Settings
+ = _('Settings')
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
@@ -271,16 +291,16 @@
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show badges#index pages#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Settings') }
+ = _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: 'General' do
%span
- General
+ = _('General')
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: 'Members' do
%span
- Members
+ = _('Members')
- if can_edit
= nav_link(controller: :badges) do
= link_to project_settings_badges_path(@project), title: _('Badges') do
@@ -290,21 +310,21 @@
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
- Integrations
+ = _('Integrations')
= nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: 'Repository' do
%span
- Repository
+ = _('Repository')
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
= link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do
%span
- CI / CD
+ = _('CI / CD')
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do
%span
- Pages
+ = _('Pages')
- else
= nav_link(controller: :project_members) do
@@ -312,12 +332,12 @@
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
- Members
+ = _('Members')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
- #{ _('Members') }
+ = _('Members')
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
new file mode 100644
index 00000000000..a8964b19ba1
--- /dev/null
+++ b/app/views/layouts/terms.html.haml
@@ -0,0 +1,34 @@
+!!! 5
+- @hide_breadcrumbs = true
+%html{ lang: I18n.locale, class: page_class }
+ = render "layouts/head"
+
+ %body{ data: { page: body_data_page } }
+ .layout-page.terms{ class: page_class }
+ .content-wrapper.prepend-top-0
+ .mobile-overlay
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = render 'layouts/header/read_only_banner'
+ = render "layouts/flash", extra_flash_class: 'limit-container-width'
+
+ %div{ class: "#{container_class} limit-container-width" }
+ .content{ id: "content-body" }
+ .panel.panel-default
+ .panel-heading
+ .title
+ = brand_header_logo
+ - logo_text = brand_header_logo_type
+ - if logo_text.present?
+ %span.logo-text.prepend-left-8
+ = logo_text
+ - if header_link?(:user_dropdown)
+ .navbar-collapse
+ %ul.nav.navbar-nav
+ %li.header-user.dropdown
+ = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
+ = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
+ = sprite_icon('angle-down', css_class: 'caret-down')
+ .dropdown-menu.dropdown-menu-right
+ = render 'layouts/header/current_user_dropdown'
+ = yield
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index b8b75da3f2f..6730172242b 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -4,7 +4,7 @@
by
= link_to member.created_by.name, user_url(member.created_by)
to join the
- = link_to member_source.human_name, member_source.web_url
+ = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token)
#{member_source.model_name.singular} as #{member.human_access}.
%p
diff --git a/app/views/notify/merge_request_unmergeable_email.html.haml b/app/views/notify/merge_request_unmergeable_email.html.haml
new file mode 100644
index 00000000000..e84905a5fad
--- /dev/null
+++ b/app/views/notify/merge_request_unmergeable_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ Merge Request #{link_to @merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)} can no longer be merged due to the following #{pluralize(@reasons, 'reason')}:
+
+ %ul
+ - @reasons.each do |reason|
+ %li= reason
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
new file mode 100644
index 00000000000..4f0901c761a
--- /dev/null
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -0,0 +1,11 @@
+Merge Request #{@merge_request.to_reference} can no longer be merged due to the following #{pluralize(@reasons, 'reason')}:
+
+- @reasons.each do |reason|
+ * #{reason}
+
+Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)}
+
+= merge_path_description(@merge_request, 'to')
+
+Author: #{@merge_request.author_name}
+Assignee: #{@merge_request.assignee_name}
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 38dab104eb5..baafaa6e3a0 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -12,7 +12,7 @@
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
@@ -114,7 +114,7 @@
= failed.size
failed
#{'build'.pluralize(failed.size)}.
-%tr.warning
+%tr.table-warning
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
%tr.section
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 7b06e8afa0b..4fe3c4c8269 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -1,4 +1,4 @@
-%tr.success
+%tr.table-success
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#31af64;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
@@ -12,7 +12,7 @@
&nbsp;
%tr.section
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %table.table-info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
diff --git a/app/views/peek/_bar.html.haml b/app/views/peek/_bar.html.haml
index b4d86e1601c..cb0cccb8f8a 100644
--- a/app/views/peek/_bar.html.haml
+++ b/app/views/peek/_bar.html.haml
@@ -3,10 +3,5 @@
#js-peek{ data: { env: Peek.env,
request_id: Peek.request_id,
peek_url: peek_routes.results_url,
- profile_url: url_for(params.merge(lineprofiler: 'true')) },
+ profile_url: url_for(safe_params.merge(lineprofiler: 'true')) },
class: Peek.env }
-
-#peek-view-performance-bar.hidden
- = render_server_response_time
- %span#serverstats
- %ul.performance-bar
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index d0ad90ac6cc..37466f7c821 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -9,6 +9,6 @@
Signed in with
= event.details[:with]
authentication
- %span.pull-right= time_ago_with_tooltip(event.created_at)
+ %span.float-right= time_ago_with_tooltip(event.created_at)
= paginate events, theme: "gitlab"
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
new file mode 100644
index 00000000000..d198bfc80db
--- /dev/null
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -0,0 +1,31 @@
+- is_current_session = active_session.current?(session)
+
+%li.list-group-item
+ .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
+ = active_session_device_type_icon(active_session)
+
+ .description.pull-left
+ %div
+ %strong= active_session.ip_address
+ - if is_current_session
+ %div This is your current session
+ - else
+ %div
+ Last accessed on
+ = l(active_session.updated_at, format: :short)
+
+ %div
+ %strong= active_session.browser
+ on
+ %strong= active_session.os
+
+ %div
+ %strong Signed in
+ on
+ = l(active_session.created_at, format: :short)
+
+ - unless is_current_session
+ .pull-right
+ = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
+ %span.sr-only Revoke
+ Revoke
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
new file mode 100644
index 00000000000..8688a52843d
--- /dev/null
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -0,0 +1,15 @@
+- page_title 'Active Sessions'
+- @content_class = "limit-container-width" unless fluid_layout
+
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
+ .col-lg-8
+ .append-bottom-default
+
+ .card.border-0
+ %ul.list-group.list-group-flush
+ = render partial: 'profiles/active_sessions/active_session', collection: @sessions
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index c7094800fb2..9e82e47c1e1 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -24,4 +24,4 @@
Never
%td
- = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
+ = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger float-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
index 5bf22aa94f1..d86941b7a29 100644
--- a/app/views/profiles/chat_names/new.html.haml
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -9,7 +9,7 @@
.actions
= form_tag profile_chat_names_path, method: :post do
= hidden_field_tag :token, @chat_name_token.token
- = submit_tag "Authorize", class: "btn btn-success wide pull-left"
+ = submit_tag "Authorize", class: "btn btn-success wide float-left"
= form_tag deny_profile_chat_names_path, method: :delete do
= hidden_field_tag :token, @chat_name_token.token
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index e3c2bd1150e..914bd4eb57c 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -32,20 +32,20 @@
%ul.well-list
%li
= render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
- %span.pull-right
- %span.label.label-success Primary email
+ %span.float-right
+ %span.badge.badge-success Primary email
- if @primary_email === current_user.public_email
- %span.label.label-info Public email
+ %span.badge.badge-info Public email
- if @primary_email === current_user.notification_email
- %span.label.label-info Notification email
+ %span.badge.badge-info Notification email
- @emails.each do |email|
%li
= render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
- %span.pull-right
+ %span.float-right
- if email.email === current_user.public_email
- %span.label.label-info Public email
+ %span.badge.badge-info Public email
- if email.email === current_user.notification_email
- %span.label.label-info Notification email
+ %span.badge.badge-info Notification email
- unless email.confirmed?
- confirm_title = "#{email.confirmation_sent_at ? 'Resend' : 'Send'} confirmation email"
= link_to confirm_title, resend_confirmation_instructions_profile_email_path(email), method: :put, class: 'btn btn-sm btn-warning prepend-left-10'
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index 5ed517c1ef6..d1fd7bc8e71 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -1,6 +1,6 @@
%li.key-list-item
- .pull-left.append-right-10
- = icon 'key', class: "settings-list-icon hidden-xs"
+ .float-left.append-right-10
+ = icon 'key', class: "settings-list-icon d-none d-sm-block"
.key-list-item-info
- key.emails_with_verified_status.map do |email, verified|
= render partial: 'shared/email_with_badge', locals: { email: email, verified: verified }
@@ -14,7 +14,7 @@
- key.subkeys.each do |subkey|
%li
%code= subkey.fingerprint
- .pull-right
+ .float-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
= link_to profile_gpg_key_path(key), data: { confirm: 'Are you sure? Removing this GPG key does not affect already signed commits.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 103446243e5..ce20994b0f4 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -1,9 +1,9 @@
%li.key-list-item
- .pull-left.append-right-10
+ .float-left.append-right-10
- if key.valid?
- = icon 'key', class: 'settings-list-icon hidden-xs'
+ = icon 'key', class: 'settings-list-icon d-none d-sm-block'
- else
- = icon 'exclamation-triangle', class: 'settings-list-icon hidden-xs has-tooltip',
+ = icon 'exclamation-triangle', class: 'settings-list-icon d-none d-sm-block has-tooltip',
title: key.errors.full_messages.join(', ')
@@ -15,7 +15,7 @@
.last-used-at
last used:
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
- .pull-right
+ .float-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
= link_to path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent prepend-left-10" do
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 77521417f47..02d5b08f7a3 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -1,8 +1,8 @@
- is_admin = defined?(admin) ? true : false
.row.prepend-top-default
.col-md-4
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
SSH Key
%ul.well-list
%li
@@ -23,5 +23,5 @@
%pre.well-pre
= @key.key
.col-md-12
- .pull-right
+ .float-right
= link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key"
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index 537bba21f4a..a12246bcdcc 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -8,5 +8,5 @@
%span.str-truncated
= link_to group.name, group_path(group)
- .pull-right
+ .float-right
= render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index 5b2a69b8891..823fec3fab4 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -8,5 +8,5 @@
%span.str-truncated
= link_to_project(project)
- .pull-right
+ .float-right
= render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 8f099aa6dd7..71ea625e5d5 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -29,7 +29,7 @@
= label_tag :global_notification_level, "Global notification level", class: "label-light"
%br
.clearfix
- .form-group.pull-left.global-notification-setting
+ .form-group.float-left.global-notification-setting
= render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index c606b5a1e6c..8a51a30191a 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -20,7 +20,7 @@
.form-group
= f.label :current_password, class: 'label-light'
= f.password_field :current_password, required: true, class: 'form-control'
- %p.help-block
+ %p.form-text.text-muted
You must provide your current password in order to change it.
.form-group
= f.label :password, 'New password', class: 'label-light'
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 2eb9fac57c3..2176d7f8a31 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -2,7 +2,7 @@
- header_title "New Password"
%h3.page-title Setup new password
%hr
-= form_for @user, url: profile_password_path, method: :post, html: { class: 'form-horizontal '} do |f|
+= form_for @user, url: profile_password_path, method: :post do |f|
%p.slead
Please set a new password before proceeding.
%br
@@ -11,14 +11,14 @@
= form_errors(@user)
- unless @user.password_automatically_set?
- .form-group
- = f.label :current_password, class: 'control-label'
+ .form-group.row
+ = f.label :current_password, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :current_password, required: true, class: 'form-control'
- .form-group
- = f.label :password, class: 'control-label'
+ .form-group.row
+ = f.label :password, class: 'col-form-label col-sm-2'
.col-sm-10= f.password_field :password, required: true, class: 'form-control'
- .form-group
- = f.label :password_confirmation, class: 'control-label'
+ .form-group.row
+ = f.label :password_confirmation, class: 'col-form-label col-sm-2'
.col-sm-10
= f.password_field :password_confirmation, required: true, class: 'form-control'
.form-actions
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 9b87a7aaca8..d253e8e456e 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -20,9 +20,9 @@
.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-btn
- = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left", class: "btn-default btn-clipboard")
- %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
+ %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
@@ -42,7 +42,7 @@
.col-lg-8.rss-token-reset
= label_tag :rss_token, 'RSS token', class: "label-light"
= text_field_tag :rss_token, current_user.rss_token, class: 'form-control', readonly: true, onclick: 'this.select()'
- %p.help-block
+ %p.form-text.text-muted
Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds as if they were you.
You should
= link_to 'reset it', [:reset, :rss_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS URLs currently in use will stop working.' }
@@ -61,7 +61,7 @@
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, 'Incoming email token', class: "label-light"
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
- %p.help-block
+ %p.form-text.text-muted
Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
You should
= link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 6aefd97bb96..ab5565cfdaf 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -51,7 +51,7 @@
= f.label :layout, class: 'label-light' do
Layout width
= f.select :layout, layout_choices, {}, class: 'form-control'
- .help-block
+ .form-text.text-muted
Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group
= f.label :dashboard, class: 'label-light' do
@@ -61,7 +61,7 @@
= f.label :project_view, class: 'label-light' do
Project overview content
= f.select :project_view, project_view_choices, {}, class: 'form-control'
- .help-block
+ .form-text.text-muted
Choose what content you want to see on a project’s overview page
.form-group
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index e497eab32e0..fbb29e7a0d9 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -26,7 +26,7 @@
%button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...")
%span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen")
= f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
- .help-block= _("The maximum file size allowed is 200KB.")
+ .form-text.text-muted= _("The maximum file size allowed is 200KB.")
- if @user.avatar?
%hr
= link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
index 43b58be7f98..93722d7b034 100644
--- a/app/views/profiles/two_factor_auths/_codes.html.haml
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -4,7 +4,7 @@
%b will
lose access to your account.
-.codes.well
+.codes.card
%ul
- @codes.each do |code|
%li
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index d1eae05c46c..e35ebdea435 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -86,7 +86,7 @@
%tr
%td= registration.name.presence || "<no name set>"
%td= registration.created_at.to_date.to_s(:medium)
- %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
+ %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
- else
.settings-message.text-center
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 043057e79ee..075badb9e56 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -37,7 +37,7 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- %span.hidden-xs
+ %span.d-none.d-sm-inline
- if can?(current_user, :download_code, @project)
.project-clone-holder
= render "shared/clone_panel"
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
new file mode 100644
index 00000000000..4bee6cb97eb
--- /dev/null
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -0,0 +1,51 @@
+- active_tab = local_assigns.fetch(:active_tab, 'blank')
+- f = local_assigns.fetch(:f)
+
+.project-import.row
+ .col-lg-12
+ .form-group.import-btn-container.clearfix
+ = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
+ Import project from
+ .import-buttons
+ - if gitlab_project_import_enabled?
+ .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = icon('gitlab', text: 'GitLab export')
+ %div
+ - if github_import_enabled?
+ = link_to new_import_github_path, class: 'btn js-import-github' do
+ = icon('github', text: 'GitHub')
+ %div
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = icon('bitbucket', text: 'Bitbucket')
+ - unless bitbucket_import_configured?
+ = render 'bitbucket_import_modal'
+ %div
+ - if gitlab_import_enabled?
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
+ = icon('gitlab', text: 'GitLab.com')
+ - unless gitlab_import_configured?
+ = render 'gitlab_import_modal'
+ %div
+ - if google_code_import_enabled?
+ = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ = icon('google', text: 'Google Code')
+ %div
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ = icon('bug', text: 'Fogbugz')
+ %div
+ - if gitea_import_enabled?
+ = link_to new_import_gitea_path, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
+ - if git_import_enabled?
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
+ = icon('git', text: 'Repo by URL')
+ .col-lg-12
+ .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
+ %hr
+ = render "shared/import_form", f: f
+ = render 'new_project_fields', f: f, project_name_id: "import-url-name"
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
index c137e38ed50..e3dc0677bd6 100644
--- a/app/views/projects/_issuable_by_email.html.haml
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -17,8 +17,8 @@
You can create a new #{name} inside this project by sending an email to the following email address:
.email-modal-input-group.input-group
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard btn-transparent hidden-xs')
+ .input-group-append
+ = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block')
= mail_to email, class: 'btn btn-clipboard btn-transparent',
subject: _("Enter the #{name} title"),
body: _("Enter the #{name} description"),
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index f6d396c8127..3b66fdbdf1a 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,6 +1,6 @@
- event = last_push_event
- if event && show_last_push_widget?(event)
- .row-content-block.top-block.hidden-xs.white
+ .row-content-block.top-block.d-none.d-sm-block.white
.event-last-push
.event-last-push-text
%span= s_("LastPushEvent|You pushed to")
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 8212ab9a31e..8fb6aa55436 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -9,7 +9,7 @@
.md-area
.md-header
- %ul.nav-links.clearfix
+ %ul.nav.nav-tabs.nav-links.clearfix
%li.md-header-tab.active
%a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write
diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml
index f455522d17c..2f08a28e26e 100644
--- a/app/views/projects/_merge_request_fast_forward_settings.html.haml
+++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml
@@ -1,7 +1,7 @@
- form = local_assigns.fetch(:form)
- project = local_assigns.fetch(:project)
-.radio
+.form-check
= label_tag :project_merge_method_ff do
= form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff"
%strong Fast-forward merge
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index f6e5712ce81..762a263656d 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -1,7 +1,7 @@
- form = local_assigns.fetch(:form)
.form-group
- .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
+ .form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
= form.label :only_allow_merge_if_pipeline_succeeds do
= form.check_box :only_allow_merge_if_pipeline_succeeds
%strong Only allow merge requests to be merged if the pipeline succeeds
@@ -9,15 +9,15 @@
%span.descr
Pipelines need to be configured to enable this feature.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank'
- .checkbox
+ .form-check
= form.label :only_allow_merge_if_all_discussions_are_resolved do
= form.check_box :only_allow_merge_if_all_discussions_are_resolved
%strong Only allow merge requests to be merged if all discussions are resolved
- .checkbox
+ .form-check
= form.label :resolve_outdated_diff_discussions do
= form.check_box :resolve_outdated_diff_discussions
%strong Automatically resolve merge request diff discussions when they become outdated
- .checkbox
+ .form-check
= form.label :printing_merge_request_link_enabled do
= form.check_box :printing_merge_request_link_enabled
%strong Show link to create/view merge request when pushing from the command line
diff --git a/app/views/projects/_merge_request_rebase_settings.html.haml b/app/views/projects/_merge_request_rebase_settings.html.haml
index 54e0b73d24c..93895a55435 100644
--- a/app/views/projects/_merge_request_rebase_settings.html.haml
+++ b/app/views/projects/_merge_request_rebase_settings.html.haml
@@ -1,6 +1,6 @@
- form = local_assigns.fetch(:form)
-.radio
+.form-check
= label_tag :project_merge_method_rebase_merge do
= form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio"
%strong Merge commit with semi-linear history
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index fd0c419cdac..a9ddcd94865 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -3,7 +3,7 @@
.form-group
= label_tag :merge_method_merge, class: 'label-light' do
Merge method
- .radio
+ .form-check
= label_tag :project_merge_method_merge do
= form.radio_button :merge_method, :merge, class: "js-merge-method-radio"
%strong Merge commit
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 241bc3dbca0..6366a2f729a 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -9,13 +9,15 @@
Project path
.input-group
- if current_user.can_select_namespace?
- .input-group-addon.has-tooltip{ title: root_url }
- = root_url
+ .input-group-prepend.has-tooltip{ title: root_url }
+ .input-group-text
+ = root_url
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1}
- else
- .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- #{user_url(current_user.username)}/
+ .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
+ .input-group-text
+ #{user_url(current_user.username)}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.project-path.col-sm-6
= f.label :path, class: 'label-light' do
@@ -23,7 +25,7 @@
Project name
= f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true
- if current_user.can_create_group?
- .help-block
+ .form-text.text-muted
Want to house several dependent projects under the same namespace?
= link_to "Create a group", new_group_path
diff --git a/app/views/projects/_new_project_push_tip.html.haml b/app/views/projects/_new_project_push_tip.html.haml
index 9bc69211d12..22e9522c0e7 100644
--- a/app/views/projects/_new_project_push_tip.html.haml
+++ b/app/views/projects/_new_project_push_tip.html.haml
@@ -3,9 +3,9 @@
= label_tag(:push_to_create_tip, _("Private projects can be created in your personal namespace with:"), class: "weight-normal")
%p.input-group.project-tip-command
- %span.input-group-btn
+ %span
= text_field_tag :push_to_create_tip, push_to_create_project_command, class: "js-select-on-focus form-control monospace", readonly: true, aria: { label: _("Push project from command line") }
- %span.input-group-btn
- = clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), placement: "right")
+ %span.input-group-append
+ = clipboard_button(text: push_to_create_project_command, title: _("Copy command to clipboard"), class: 'input-group-text', placement: "right")
%p
= link_to("What does this command do?", help_page_path("gitlab-basics/create-project", anchor: "push-to-create-a-new-project"), target: "_blank")
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
index d50175727be..9d27f51926e 100644
--- a/app/views/projects/_project_templates.html.haml
+++ b/app/views/projects/_project_templates.html.haml
@@ -14,11 +14,12 @@
%label.label-light
Template
.input-group.template-input-group
- .input-group-addon
- .selected-icon
- - Gitlab::ProjectTemplate.all.each do |template|
- = custom_icon(template.logo)
- .selected-template
+ .input-group-prepend
+ .input-group-text
+ .selected-icon
+ - Gitlab::ProjectTemplate.all.each do |template|
+ = custom_icon(template.logo)
+ .selected-template
%button.btn.btn-default.change-template{ type: "button" } Change template
= render 'new_project_fields', f: f, project_name_id: "template-project-name"
diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index a115b65938b..8bffd1396ae 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -1,8 +1,8 @@
- anchors = local_assigns.fetch(:anchors, [])
- return unless anchors.any?
-%ul.nav
+%ul.nav.justify-content-center
- anchors.each do |anchor|
- %li
- = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'stat-link' : "btn btn-#{anchor.class_modifier || 'missing'}" do
+ %li.nav-item
+ = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do
%span.stat-text= anchor.label
diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml
index a56c3503c77..5646dc464f8 100644
--- a/app/views/projects/_wiki.html.haml
+++ b/app/views/projects/_wiki.html.haml
@@ -1,6 +1,6 @@
- if @wiki_home.present?
%div{ class: container_class }
- .wiki-holder.prepend-top-default.append-bottom-default
+ .prepend-top-default.append-bottom-default
.wiki
= render_wiki_content(@wiki_home)
- else
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index 2942d618a42..aac7a1870df 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -5,11 +5,11 @@
.tree-holder
.nav-block
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build)
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
- %li
+ %li.breadcrumb-item
- if path == @path
= link_to file_project_job_artifacts_path(@project, @build, path) do
%strong= title
@@ -22,7 +22,7 @@
.js-file-title.file-title-flex-parent
= render 'projects/blob/header_content', blob: blob
- .file-actions.hidden-xs
+ .file-actions.d-none.d-sm-block
= render 'projects/blob/viewer_switcher', blob: blob
.btn-group{ role: "group" }<
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index e45861ac08d..e90916e340d 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -22,7 +22,7 @@
.commit-row-title
%span.item-title.str-truncated-100
= link_to_markdown commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
- .pull-right
+ .float-right
= link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha"
&nbsp;
.light
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 849716a679b..a4b1b496b69 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,6 +1,6 @@
= render "projects/blob/breadcrumb", blob: blob
-.info-well.hidden-xs
+.info-well.d-none.d-sm-block
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref
diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml
index 1c148de9678..a4fb5f6ba88 100644
--- a/app/views/projects/blob/_breadcrumb.html.haml
+++ b/app/views/projects/blob/_breadcrumb.html.haml
@@ -5,12 +5,12 @@
= render 'shared/ref_switcher', destination: 'blob', path: @path
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to project_tree_path(@project, @ref) do
= @project.path
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
- %li
+ %li.breadcrumb-item
- if path == @path
= link_to project_blob_path(@project, tree_join(@ref, path)) do
%strong= title
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index c9fa90acd11..8560b72fe85 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -16,7 +16,7 @@
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name js-file-path-name-input'
- .pull-right.file-buttons
+ .float-right.file-buttons
= button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap
= custom_icon('icon_no_wrap')
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 5d457a50c49..4bef45932d0 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -10,4 +10,4 @@
= number_to_human_size(blob.raw_size)
- if blob.stored_externally? && blob.external_storage == :lfs
- %span.label.label-lfs.append-right-5 LFS
+ %span.badge.label-lfs.append-right-5 LFS
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index 48ff66900be..e7a4e3d67cb 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -5,9 +5,9 @@
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title= _('Create New Directory')
.modal-body
- = form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-quick-submit js-requires-input' do
- .form-group
- = label_tag :dir_name, _('Directory name'), class: 'control-label'
+ = form_tag project_create_dir_path(@project, @id), method: :post, remote: false, class: 'js-create-dir-form js-quick-submit js-requires-input' do
+ .form-group.row
+ = label_tag :dir_name, _('Directory name'), class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control'
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index 750bdef3308..4628ecff3d6 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -6,10 +6,10 @@
%h3.page-title Delete #{@blob.name}
.modal-body
- = form_tag project_blob_path(@project, @id), method: :delete, class: 'form-horizontal js-delete-blob-form js-quick-submit js-requires-input' do
+ = form_tag project_blob_path(@project, @id), method: :delete, class: 'js-delete-blob-form js-quick-submit js-requires-input' do
= render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-sm-2.col-sm-10
= button_tag 'Delete file', class: 'btn btn-remove btn-remove-file'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 182d02376bf..60a49441ce8 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -5,7 +5,7 @@
%a.close{ href: "#", "data-dismiss" => "modal" } &times;
%h3.page-title= title
.modal-body
- = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal', data: { method: method } do
+ = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form', data: { method: method } do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 9d90251ab66..27cf040da7c 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -15,7 +15,7 @@
Edit file
= render 'template_selectors'
.file-editor
- %ul.nav-links.no-bottom.js-edit-mode
+ %ul.nav-links.no-bottom.js-edit-mode.nav.nav-tabs
%li.active
= link_to '#editor' do
Write
@@ -24,7 +24,7 @@
= link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do
= editing_preview_title(@blob.name)
- = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
+ = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index fa091d8f6ef..39442564a2b 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -7,7 +7,7 @@
New file
= render 'template_selectors'
.file-editor
- = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
+ = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index d0c01f95cb7..5b1f2d8953b 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -8,18 +8,17 @@
%li{ class: "branch-item js-branch-#{branch.name}" }
.branch-info
.branch-title
- = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name' do
- = sprite_icon('fork', size: 12)
+ = sprite_icon('fork', size: 12)
+ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do
= branch.name
- &nbsp;
- if branch.name == @repository.root_ref
- %span.label.label-primary default
+ %span.badge.badge-primary.prepend-left-5 default
- elsif merged
- %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
+ %span.badge.badge-info.has-tooltip.prepend-left-5{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged')
- if protected_branch?(@project, branch)
- %span.label.label-success
+ %span.badge.badge-success.prepend-left-5
= s_('Branches|protected')
.block-truncated
@@ -29,7 +28,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
- .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ .divergence-graph.d-none.d-sm-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
@@ -40,7 +39,7 @@
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
%span.count.count-ahead= diverging_count_label(number_commits_ahead)
- .controls.hidden-xs<
+ .controls.d-none.d-sm-block<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
= _('Merge request')
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
index 12e5a8e8d69..398f76d379a 100644
--- a/app/views/projects/branches/_panel.html.haml
+++ b/app/views/projects/branches/_panel.html.haml
@@ -7,13 +7,13 @@
- return unless branches.any?
-.panel.panel-default.prepend-top-10
- .panel-heading
- %h4.panel-title
+.card.prepend-top-10
+ .card-header
+ %h4.card-title
= panel_title
%ul.content-list.all-branches
- branches.first(overview_max_branches).each do |branch|
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch)
- if branches.size > overview_max_branches
- .panel-footer.text-center
+ .card-footer.text-center
= link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state }
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 5dcc72d8263..d6568c9f64a 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -3,7 +3,7 @@
%div{ class: container_class }
.top-area.adjust
- %ul.nav-links.issues-state-filters
+ %ul.nav-links.issues-state-filters.nav.nav-tabs
%li{ class: active_when(@mode == 'overview') }>
= link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches')
@@ -26,7 +26,7 @@
%span.light
= branches_sort_options_hash[@sort]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= s_('Branches|Sort by')
- branches_sort_options_hash.each do |value, title|
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index c7fc5a98ca8..65b414c8af2 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -9,14 +9,14 @@
New Branch
%hr
-= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-create-branch-form js-requires-input" do
- .form-group
- = label_tag :branch_name, nil, class: 'control-label'
+= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "js-create-branch-form js-requires-input" do
+ .form-group.row
+ = label_tag :branch_name, nil, class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name'
- .help-block.text-danger.js-branch-name-error
- .form-group
- = label_tag :ref, 'Create from', class: 'control-label'
+ .form-text.text-muted.text-danger.js-branch-name-error
+ .form-group.row
+ = label_tag :ref, 'Create from', class: 'col-form-label col-sm-2'
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, default_ref
@@ -24,7 +24,7 @@
.text-left.dropdown-toggle-text= default_ref
= icon('chevron-down')
= render 'shared/ref_dropdown', dropdown_class: 'wide'
- .help-block Existing branch name, tag, or commit SHA
+ .form-text.text-muted Existing branch name, tag, or commit SHA
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index f49f6e630d2..c75093c4c24 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -7,7 +7,7 @@
= sprite_icon('download')
= icon("caret-down")
%span.sr-only= _('Select Archive Format')
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%li.dropdown-header
#{ _('Source code') }
%li
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 2e86a7d36d7..84245d72f4a 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -11,7 +11,7 @@
%a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') }
= icon('plus')
= icon("caret-down")
- %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
+ %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can_create_issue || merge_project || can_create_project_snippet
%li.dropdown-header= _('This project')
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 9126476e79e..44c1453e239 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -41,14 +41,14 @@
.label-container
- if job.tags.any?
- job.tags.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
- if job.try(:trigger_request)
- %span.label.label-info triggered
+ %span.badge.badge-info triggered
- if job.try(:allow_failure)
- %span.label.label-danger allowed to fail
+ %span.badge.badge-danger allowed to fail
- if job.action?
- %span.label.label-info manual
+ %span.badge.badge-info manual
- if pipeline_link
%td
@@ -93,7 +93,7 @@
#{job.coverage}%
%td
- .pull-right
+ .float-right
- if can?(current_user, :read_build, job) && job.artifacts?
= link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= sprite_icon('download')
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
index 6ca8152183d..cbda6bf2107 100644
--- a/app/views/projects/ci/lints/show.html.haml
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -3,22 +3,21 @@
- content_for :library_javascripts do
= page_specific_javascript_tag('lib/ace.js')
-%h2 Check your .gitlab-ci.yml
+%h2.pt-3.pb-3 Check your .gitlab-ci.yml
.project-ci-linter
- .row
- = form_tag project_ci_lint_path(@project), method: :post do
- .form-group
- .col-sm-12
- .file-holder
- .js-file-title.file-title.clearfix
- Content of .gitlab-ci.yml
- #ci-editor.ci-editor= @content
- = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
+ = form_tag project_ci_lint_path(@project), method: :post do
+ .row
.col-sm-12
- .pull-left.prepend-top-10
+ .file-holder
+ .js-file-title.file-title.clearfix
+ Content of .gitlab-ci.yml
+ #ci-editor.ci-editor= @content
+ = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
+ .col-sm-12
+ .float-left.prepend-top-10
= submit_tag('Validate', class: 'btn btn-success submit-yml')
- .pull-right.prepend-top-10
+ .float-right.prepend-top-10
= button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml')
.row.prepend-top-20
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 14979bee714..e9bdc54364b 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -7,7 +7,7 @@
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
- .well.form-group
+ .card.form-group
%label.text-danger
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
index 5f49d03b1bb..b8a3556a206 100644
--- a/app/views/projects/clusters/_empty_state.html.haml
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -1,7 +1,7 @@
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content= image_tag 'illustrations/clusters_empty.svg'
- .col-xs-12
+ .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')
diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
new file mode 100644
index 00000000000..d0402197821
--- /dev/null
+++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml
@@ -0,0 +1,12 @@
+- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
+.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
+ %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
+ %div
+ .col-sm-2.gcp-logo
+ = image_tag 'illustrations/logos/google-cloud-platform_logo.svg'
+ .col-sm-10
+ %h4= s_('ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform')
+ %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
+ %a.btn.btn-info{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
+ Apply for credit
+
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index 78cd687ef93..877e0cc876c 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -3,8 +3,8 @@
= s_('ClusterIntegration|Kubernetes cluster name')
.input-group
%input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
- %span.input-group-btn
- = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'btn-default')
+ %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_errors(@cluster)
@@ -14,22 +14,22 @@
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
.input-group
= platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true
- %span.input-group-btn
- = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'btn-default')
+ %span.input-group-append
+ = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
.form-group
= platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
.input-group
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true
- %span.input-group-addon.clipboard-addon
- = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'btn-blank')
+ %span.input-group-append.clipboard-addon
+ = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
.form-group
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
.input-group
= platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true
- %span.input-group-btn
- %button.btn.btn-default.js-show-cluster-token{ type: 'button' }
+ %span.input-group-append
+ %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
= s_('ClusterIntegration|Show')
= clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index dada51f39da..f1771349a53 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Login")
+= render_gcp_signup_offer
+
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
@@ -8,7 +10,7 @@
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
= render 'header'
.row
- .col-sm-8.col-sm-offset-4.signin-with-google
+ .col-sm-8.offset-sm-4.signin-with-google
- if @authorize_url
= link_to @authorize_url do
= image_tag('auth_buttons/signin_with_google.png', width: '191px')
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml
index 17b244f4bf7..a55de84b5cd 100644
--- a/app/views/projects/clusters/index.html.haml
+++ b/app/views/projects/clusters/index.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title "Kubernetes Clusters"
+= render_gcp_signup_offer
+
.clusters-container
- if @clusters.empty?
= render "empty_state"
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index e004966bdcc..828e2a84753 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title 'Kubernetes'
- page_title _("Kubernetes Cluster")
+= render_gcp_signup_offer
+
.row.prepend-top-default
.col-sm-4
= render 'sidebar'
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index ebbf7e775c7..77d7a055474 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -17,9 +17,10 @@
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
.input-group
= platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
- %span.input-group-addon.clipboard-addon
- %button.js-show-cluster-token.btn-blank{ type: 'button' }
- = s_('ClusterIntegration|Show')
+ %span.input-group-append.clipboard-addon
+ .input-group-text
+ %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)')
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index 36b28c731a1..eb677cff5f0 100644
--- a/app/views/projects/commit/_ajax_signature.html.haml
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -1,2 +1,2 @@
- if commit.has_signature?
- %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
+ %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 21e4664d4e4..430bc8f59f9 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -20,9 +20,9 @@
.modal-body
- if description
%p.append-bottom-20= description
- = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
- .form-group.branch
- = label_tag 'start_branch', branch_label, class: 'control-label'
+ = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do
+ .form-group.row.branch
+ = label_tag 'start_branch', branch_label, class: 'col-form-label col-sm-2'
.col-sm-10
= hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch'
= dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } })
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index 7338468967f..f6666921a25 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,10 +1,10 @@
-%ul.nav-links.no-top.no-bottom.commit-ci-menu
+%ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs
= nav_link(path: 'commit#show') do
= link_to project_commit_path(@project, @commit.id) do
Changes
- %span.badge= @diffs.size
+ %span.badge.badge-pill= @diffs.size
- if can?(current_user, :read_pipeline, @project)
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_project_commit_path(@project, @commit.id) do
Pipelines
- %span.badge.js-pipelines-mr-count= @commit.pipelines.size
+ %span.badge.badge-pill.js-pipelines-mr-count= @commit.pipelines.size
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 213c4c90a0e..886dd73c33b 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -7,7 +7,7 @@
#{ s_('CommitBoxTitle|Commit') }
%span.commit-sha= @commit.short_id
= clipboard_button(text: @commit.id, title: _("Copy commit SHA to clipboard"))
- %span.hidden-xs authored
+ %span.d-none.d-sm-inline authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span= s_('ByAuthor|by')
= author_avatar(@commit, size: 24)
@@ -21,17 +21,17 @@
.header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
- %span.btn.disabled.btn-grouped.hidden-xs.append-right-10
+ %span.btn.disabled.btn-grouped.d-none.d-sm-block.append-right-10
= icon('comment')
= @notes_count
- = link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 hidden-xs hidden-sm" do
+ = link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 d-none d-sm-none d-md-inline" do
#{ _('Browse files') }
.dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
%span= _('Options')
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li.visible-xs-block.visible-sm-block
+ %ul.dropdown-menu.dropdown-menu-right
+ %li.d-block.d-sm-none.d-md-none
= link_to project_tree_path(@project, @commit) do
#{ _('Browse Files') }
- if can_collaborate && !@commit.has_been_reverted?(current_user)
@@ -54,7 +54,7 @@
%h3.commit-title
= markdown_field(@commit, :title)
- if @commit.description.present?
- %pre.commit-description
+ .commit-description<
= preserve(markdown_field(@commit, :description))
.info-well
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index aac020b42c5..c4d986ef742 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -24,5 +24,5 @@
= link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
-%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
+%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml
index 8611129b356..a91e31afc2b 100644
--- a/app/views/projects/commit/branches.html.haml
+++ b/app/views/projects/commit/branches.html.haml
@@ -6,7 +6,8 @@
- if @branches.any? || @tags.any? || @tags_limit_exceeded
%span
- = link_to "…", "#", class: "js-details-expand label label-gray"
+ = link_to "#", class: "js-details-expand label label-gray ref-name" do
+ = sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle')
%span.js-details-content.hide
= commit_branches_links(@project, @branches)
- if @tags_limit_exceeded
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 3fd0fa348b3..12b27eb9b66 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -18,7 +18,7 @@
= cache(cache_key, expires_in: 1.day) do
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .avatar-cell.hidden-xs
+ .avatar-cell.d-none.d-sm-block
= author_avatar(commit, size: 36)
.commit-detail.flex-list
@@ -27,18 +27,14 @@
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title"
- else
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
- %span.commit-row-message.visible-xs-inline
+ %span.commit-row-message.d-block.d-sm-none
&middot;
= commit.short_id
- if commit.status(ref)
- .visible-xs-inline
+ .d-block.d-sm-none
= render_commit_status(commit, ref: ref)
- if commit.description?
- %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ...
-
- - if commit.description?
- %pre.commit-row-description.js-toggle-content
- = preserve(markdown_field(commit, :description))
+ %button.text-expander.d-none.d-sm-inline-block.js-toggle-button{ type: "button" } ...
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
@@ -46,7 +42,11 @@
- commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
- .commit-actions.flex-row.hidden-xs
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content.prepend-top-8.append-bottom-8
+ = preserve(markdown_field(commit, :description))
+
+ .commit-actions.flex-row.d-none.d-sm-flex
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
- else
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index 6f5835cb9be..8f8eb2c3d5a 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -1,8 +1,8 @@
- commits, hidden = limited_commits(@commits)
- commits = Commit.decorate(commits, @project)
-.panel.panel-default
- .panel-heading
+.card
+ .card-header
Commits (#{@commits.count})
- if hidden > 0
%ul.content-list
diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml
index 26385d2f534..caaff082cc3 100644
--- a/app/views/projects/commits/_inline_commit.html.haml
+++ b/app/views/projects/commits/_inline_commit.html.haml
@@ -4,5 +4,5 @@
&nbsp;
%span.str-truncated
= link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message")
- .pull-right
+ .float-right
#{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index ab371521840..9d254463fb6 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -15,7 +15,7 @@
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
- .tree-controls.hidden-xs.hidden-sm
+ .tree-controls.d-none.d-sm-none.d-md-block
- if @merge_request.present?
.control
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn'
@@ -24,7 +24,7 @@
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
- = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form', data: { 'signatures-path' => namespace_project_signatures_path }) do
+ = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d0c8a699608..07112c98804 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,28 +1,29 @@
-= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input' do
- .clearfix
- - if params[:to] && params[:from]
- .compare-switch-container
- = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
- .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
- .input-group.inline-input-group
- %span.input-group-addon
+= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline js-requires-input js-signature-container', data: { 'signatures-path' => signatures_namespace_project_compare_index_path } do
+ - if params[:to] && params[:from]
+ .compare-switch-container
+ = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
+ .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
+ .input-group.inline-input-group
+ %span.input-group-prepend
+ .input-group-text
= s_("CompareBranches|Source")
- = hidden_field_tag :to, params[:to]
- = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
- .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag")
- = render 'shared/ref_dropdown'
- .compare-ellipsis.inline ...
- .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
- .input-group.inline-input-group
- %span.input-group-addon
+ = hidden_field_tag :to, params[:to]
+ = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag")
+ = render 'shared/ref_dropdown'
+ .compare-ellipsis.inline ...
+ .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
+ .input-group.inline-input-group
+ %span.input-group-prepend
+ .input-group-text
= s_("CompareBranches|Target")
- = hidden_field_tag :from, params[:from]
- = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
- .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
- = render 'shared/ref_dropdown'
- &nbsp;
- = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn"
- - if @merge_request.present?
- = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn'
- - elsif create_mr_button?
- = link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn'
+ = hidden_field_tag :from, params[:from]
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
+ = render 'shared/ref_dropdown'
+ &nbsp;
+ = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn"
+ - if @merge_request.present?
+ = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn'
+ - elsif create_mr_button?
+ = link_to _("Create merge request"), create_mr_path, class: 'prepend-left-10 btn'
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 8da55664878..b6bebbabed0 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -10,7 +10,7 @@
= render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else
- .light-well
+ .card.bg-light
.center
%h4
= s_("CompareBranches|There isn't anything to compare.")
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
index 9007f2c24ba..5b0d73b8c68 100644
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -1,7 +1,7 @@
.cycle-analytics-overview
.container
.row
- .col-md-10.col-md-offset-1
+ .col-md-10.offset-md-1
.row.overview-details
.col-md-6.overview-text
%h4 Introducing Cycle Analytics
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 5041f322612..bdf021fd87f 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -8,21 +8,21 @@
"v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
= icon("spinner spin", "v-show" => "isLoading")
.wrapper{ "v-show" => "!isLoading && !hasError" }
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
{{ __('Pipeline Health') }}
.content-block
.container-fluid
.row
- .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" }
+ .col-sm-3.col-12.column{ "v-for" => "item in state.summary" }
%h3.header {{ item.value }}
%p.text {{ item.title }}
- .col-sm-3.col-xs-12.column
+ .col-sm-3.col-12.column
.dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
%i.fa.fa-chevron-down
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
%a{ "href" => "#", "data-value" => "7" }
{{ n__('Last %d day', 'Last %d days', 7) }}
@@ -33,8 +33,8 @@
%a{ "href" => "#", "data-value" => "90" }
{{ n__('Last %d day', 'Last %d days', 90) }}
.stage-panel-container
- .panel.panel-default.stage-panel
- .panel-heading
+ .card.stage-panel
+ .card-header
%nav.col-headers
%ul
%li.stage-header
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index c363180d0db..5ad8091a02b 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -1,24 +1,24 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
= form_errors(@deploy_keys.new_key)
- .form-group
+ .form-group.row
= f.label :title, class: "label-light"
= f.text_field :title, class: 'form-control', required: true
- .form-group
+ .form-group.row
= f.label :key, class: "label-light"
= f.text_area :key, class: "form-control", rows: 5, required: true
- .form-group
+ .form-group.row
%p.light.append-bottom-0
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README")
= f.fields_for :deploy_keys_projects do |deploy_keys_project_form|
- .form-group
- .checkbox
- = deploy_keys_project_form.label :can_push do
- = deploy_keys_project_form.check_box :can_push
- %strong Write access allowed
- .form-group
+ .form-group.row
+ = deploy_keys_project_form.label :can_push do
+ = deploy_keys_project_form.check_box :can_push
+ %strong Write access allowed
+ .form-group.row
%p.light.append-bottom-0
Allow this key to push to repository as well? (Default only allows pull access.)
- = f.submit "Add key", class: "btn-create btn"
+ .form-group.row
+ = f.submit "Add key", class: "btn-create btn"
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 7dd8dc28e5b..6af57d3ab26 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -12,4 +12,4 @@
Create a new deploy key for this project
= render @deploy_keys.form_partial_path
%hr
- #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project) } }
+ #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index cd910b82b57..e009b6fef0e 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -3,7 +3,7 @@
%hr
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'form-horizontal js-requires-input' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'js-requires-input' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
= f.submit 'Save changes', class: 'btn-save btn'
diff --git a/app/views/projects/deploy_tokens/_revoke_modal.html.haml b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
index 085964fe22e..ace3480c815 100644
--- a/app/views/projects/deploy_tokens/_revoke_modal.html.haml
+++ b/app/views/projects/deploy_tokens/_revoke_modal.html.haml
@@ -2,7 +2,7 @@
.modal-dialog
.modal-content
.modal-header
- %h4.modal-title.pull-left
+ %h4.modal-title.float-left
= s_('DeployTokens|Revoke')
%b #{token.name}?
%button.close{ 'aria-label' => _('Close'), 'data-dismiss' => 'modal', type: 'button' }
diff --git a/app/views/projects/deploy_tokens/_table.html.haml b/app/views/projects/deploy_tokens/_table.html.haml
index 5013a9b250d..91466a6736b 100644
--- a/app/views/projects/deploy_tokens/_table.html.haml
+++ b/app/views/projects/deploy_tokens/_table.html.haml
@@ -24,7 +24,7 @@
- else
%span.token-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
- %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger pull-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
+ %td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger float-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
= render 'projects/deploy_tokens/revoke_modal', token: token, project: project
- else
.settings-message.text-center
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index e2baaa625ae..e0ecf56525a 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -6,7 +6,7 @@
%button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
- actions.each do |action|
- next unless can?(current_user, :update_build, action)
%li
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 376f672f424..077c6c68f7e 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -6,16 +6,16 @@
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) }
.files-changed-inner
- .inline-parallel-buttons.hidden-xs.hidden-sm
+ .inline-parallel-buttons.d-none.d-sm-none.d-md-block
- if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
- = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
+ = link_to 'Expand all', url_for(safe_params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
- if current_controller?(:commit)
- = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
+ = commit_diff_whitespace_link(diffs.project, @commit, class: 'd-none d-sm-inline-block')
- elsif current_controller?('projects/merge_requests/diffs')
- = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs')
+ = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'd-none d-sm-inline-block')
- elsif current_controller?(:compare)
- = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs')
+ = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'd-none d-sm-inline-block')
.btn-group
= inline_diff_btn
= parallel_diff_btn
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 47bfcb21cf4..b4df654c839 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -10,7 +10,7 @@
- unless diff_file.submodule?
- blob = diff_file.blob
- .file-actions.hidden-xs
+ .file-actions.d-none.d-sm-block
- if blob&.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index dbeddf6689a..4cb04d744dc 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -37,4 +37,4 @@
#{diff_file.a_mode} → #{diff_file.b_mode}
- if diff_file.stored_externally? && diff_file.external_storage == :lfs
- %span.label.label-lfs.append-right-5 LFS
+ %span.badge.label-lfs.append-right-5 LFS
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 6fd6018dea3..aa1112c3313 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -2,7 +2,7 @@
- sum_removed_lines = diff_files.sum(&:removed_lines)
.commit-stat-summary.dropdown
Showing
- %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown" } }<
+ %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }<
= pluralize(diff_files.size, "changed file")
= icon("caret-down", class: "prepend-left-5")
%span.diff-stats-additions-deletions-expanded#diff-stats
@@ -10,7 +10,7 @@
%strong.cgreen= pluralize(sum_added_lines, 'addition')
and
%strong.cred= pluralize(sum_removed_lines, 'deletion')
- .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
+ .diff-stats-additions-deletions-collapsed.float-right.d-none.d-sm-none{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
%strong.cgreen<
+#{sum_added_lines}
%strong.cred<
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index da34a83d8e0..abe494f2974 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -1,7 +1,7 @@
.alert.alert-warning
%h4
Too many changes to show.
- .pull-right
+ .float-right
- if current_controller?(:commit)
= link_to "Plain diff", project_commit_path(@project, @commit, format: :diff), class: "btn btn-sm"
= link_to "Email patch", project_commit_path(@project, @commit, format: :patch), class: "btn btn-sm"
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 0994498c6be..79b22766bc7 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -40,7 +40,7 @@
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
- %p.help-block Separate tags with commas.
+ %p.form-text.text-muted Separate tags with commas.
%fieldset.features
%h5.prepend-top-0= _("Project avatar")
.form-group
@@ -54,7 +54,7 @@
%button.btn.js-choose-project-avatar-button{ type: 'button' }= _("Choose file...")
%span.file_name.prepend-left-default.js-avatar-filename= _("No file chosen")
= f.file_field :avatar, class: "js-project-avatar-input hidden"
- .help-block= _("The maximum file size allowed is 200KB.")
+ .form-text.text-muted= _("The maximum file size allowed is 200KB.")
- if @project.avatar?
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
@@ -142,8 +142,9 @@
%span Path
.form-group
.input-group
- .input-group-addon
- #{URI.join(root_url, @project.namespace.full_path)}/
+ .input-group-prepend
+ .input-group-text
+ #{URI.join(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 b15fe514a08..bf6fc8af12d 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -22,7 +22,7 @@
%hr
%p
- - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
+ - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
@@ -44,43 +44,49 @@
.git-empty
%fieldset
%h5 Git global setup
- %pre.light-well
+ %pre.card.bg-light
:preserve
git config --global user.name "#{h git_user_name}"
git config --global user.email "#{h git_user_email}"
%fieldset
%h5 Create a new repository
- %pre.light-well
+ %pre.card.bg-light
:preserve
git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
cd #{h @project.path}
touch README.md
git add README.md
git commit -m "add README"
- git push -u origin master
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
%fieldset
%h5 Existing folder
- %pre.light-well
+ %pre.card.bg-light
:preserve
cd existing_folder
git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add .
git commit -m "Initial commit"
- git push -u origin master
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
%fieldset
%h5 Existing Git repository
- %pre.light-well
+ %pre.card.bg-light
:preserve
cd existing_repo
git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git push -u origin --all
- git push -u origin --tags
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin --all
+ git push -u origin --tags
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index a3467eb6f05..a966bfb2dd9 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -5,10 +5,10 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'find_file', path: @path
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to project_tree_path(@project, @ref) do
= @project.path
- %li.file-finder
+ %li.file-finder.breadcrumb-item
%input#file_find.form-control.file-finder-input{ type: "text", placeholder: _('Find by path'), autocomplete: 'off' }
.tree-content-holder
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 21a4702a2a9..57afc7ac9c3 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -16,7 +16,7 @@
- else
= sort_title_recently_created
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
+ %ul.dropdown-menu.dropdown-menu-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
= link_to page_filter_path(sort: sort_value_recently_created, without: excluded_filters) do
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 475c6ba4d3d..a603b1024eb 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -12,7 +12,7 @@
- if @namespaces.present?
.fork-thumbnail-container.js-fork-content
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
- Click to fork the project
+ = _("Select a namespace to fork the project")
- @namespaces.each do |namespace|
= render 'fork_button', namespace: namespace
- else
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 620fd1906ba..639efd34a74 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -35,10 +35,10 @@
.label-container
- if generic_commit_status.tags.any?
- generic_commit_status.tags.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
- if retried
- %span.label.label-warning retried
+ %span.badge.badge-warning retried
- if pipeline_link
%td
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 14c47a5d91c..983cb187c2f 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -14,7 +14,7 @@
= icon('circle')
&nbsp;
= language[:label]
- .pull-right
+ .float-right
= language[:value]
\%
.col-md-8
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index c81ee6874e3..f1b14d4c4d1 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -24,4 +24,4 @@
.graphs.row
#contributors-master
#contributors.clearfix
- %ol.contributors-list.clearfix
+ %ol.contributors-list.row
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index 8096d9530c3..3e54c3ca9f8 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -18,8 +18,8 @@
%tr
%td
= render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
- %td.hidden-xs
- %span.label.label-gray.deploy-project-label
+ %td.d-none.d-sm-block
+ %span.badge.badge-gray.deploy-project-label
= hook_log.trigger.singularize.titleize
%td
= truncate(hook_log.url, length: 50)
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index 1cf4105bd27..e51efa85df0 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -4,6 +4,6 @@
Request details
.col-lg-9
- = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default pull-right prepend-left-10"
+ = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), class: "btn btn-default float-right prepend-left-10"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index dcc1f0e3fbe..c31aef60453 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -13,7 +13,7 @@
= f.submit 'Save changes', class: 'btn btn-create'
= render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: @hook
- = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' }
+ = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' }
%hr
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index 778ff91362d..16c4f21279d 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -5,14 +5,14 @@
%hr
- if @project.import_failed?
- .panel.panel-danger
- .panel-heading The repository could not be imported.
- .panel-body
+ .card.bg-danger
+ .card-header The repository could not be imported.
+ .card-body
%pre
:preserve
#{h(sanitize_repo_path(@project, @project.import_error))}
-= form_for @project, url: project_import_path(@project), method: :post, html: { class: 'form-horizontal' } do |f|
+= form_for @project, url: project_import_path(@project), method: :post do |f|
= render "shared/import_form", f: f
.form-actions
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 8b011af78eb..2c5ffd85372 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,2 +1,2 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form common-note-form js-quick-submit js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 0c58dd60e2c..8a14146cb87 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -12,25 +12,25 @@
= confidential_icon(issue)
= link_to issue.title, issue_path(issue)
- if issue.tasks?
- %span.task-status.hidden-xs
+ %span.task-status.d-none.d-sm-inline-block
&nbsp;
= issue.task_status
.issuable-info
%span.issuable-reference
#{issuable_reference(issue)}
- %span.issuable-authored.hidden-xs
+ %span.issuable-authored.d-none.d-sm-inline-block
&middot;
opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
by #{link_to_member(@project, issue.author, avatar: false)}
- if issue.milestone
- %span.issuable-milestone.hidden-xs
+ %span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
- = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do
+ = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
- %span.issuable-due-date.hidden-xs.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') }
+ %span.issuable-due-date.d-none.d-sm-inline-block.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') }
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
@@ -50,5 +50,5 @@
= render 'shared/issuable_meta_data', issuable: issue
- .pull-right.issuable-updated-at.hidden-xs
+ .float-right.issuable-updated-at.d-none.d-sm-inline-block
%span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 4b8bf578b28..76438fae663 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -14,7 +14,7 @@
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
- .btn-group.available.hide
+ .btn-group.available.hidden
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
@@ -22,7 +22,7 @@
= icon('caret-down')
.droplab-dropdown
- %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
+ %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
.menu-item
@@ -40,13 +40,13 @@
%label{ for: 'new-branch-name' }
= _('Branch name')
%input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
- %span.js-branch-message.help-block
+ %span.js-branch-message.form-text.text-muted
.form-group
%label{ for: 'source-name' }
= _('Source (branch or tag)')
%input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
- %span.js-ref-message.help-block
+ %span.js-ref-message.form-text.text-muted
.form-group
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f1fc1c2316d..2d036bd4e3e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -12,12 +12,12 @@
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) }
- = sprite_icon('mobile-issue-close', size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
- %span.hidden-xs
+ = sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none')
+ %span.d-none.d-sm-block
Closed
.issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) }
- = sprite_icon('issue-open-m', size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
- %span.hidden-xs Open
+ = sprite_icon('issue-open-m', size: 16, css_class: 'd-block d-sm-none')
+ %span.d-none.d-sm-block Open
.issuable-meta
- if @issue.confidential
@@ -26,15 +26,15 @@
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue")
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ %button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
%ul
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
@@ -51,9 +51,9 @@
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam
- = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
- = link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml
index 311934d9c33..ea552c73c92 100644
--- a/app/views/projects/jobs/_empty_state.html.haml
+++ b/app/views/projects/jobs/_empty_state.html.haml
@@ -5,10 +5,10 @@
- action = local_assigns.fetch(:action, nil)
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content{ class: illustration_size }
= image_tag illustration
- .col-xs-12
+ .col-12
.text-content
%h4.text-center= title
- if content
diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml
index 83a2af1dc74..b83e8dddccb 100644
--- a/app/views/projects/jobs/_header.html.haml
+++ b/app/views/projects/jobs/_header.html.haml
@@ -27,5 +27,5 @@
= link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
= link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post
- %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
+ %button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 0b57ebedebd..9c9e4ef8fce 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,15 +1,8 @@
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
- .block
- %strong.inline.prepend-top-8
- = @build.name
- - if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'js-retry-button pull-right btn btn-inverted-secondary btn-retry visible-md-block visible-lg-block', method: :post
- %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
- = icon('angle-double-right')
- #js-details-block-vue
+ #js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } }
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block
@@ -22,10 +15,10 @@
- elsif @build.has_expiring_artifacts?
%p.build-detail-row
The artifacts will be removed in
- %span= time_ago_in_words @build.artifacts_expire_at
+ %span= time_ago_with_tooltip @build.artifacts_expire_at
- if @build.artifacts?
- .btn-group.btn-group-justified{ role: :group }
+ .btn-group.btn-group.d-flex{ role: :group }
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
@@ -49,7 +42,7 @@
- if @build.trigger_variables.any?
%p
- %button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables
+ %button.btn.group.btn-group.js-reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable|
@@ -90,7 +83,7 @@
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- tooltip = build.tooltip_message
- = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: true, title: tooltip, container: 'body' }) do
+ = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', html: 'true', title: tooltip, container: 'body' }) do
= sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
diff --git a/app/views/projects/jobs/_user.html.haml b/app/views/projects/jobs/_user.html.haml
index 461d503f95d..90ce581a903 100644
--- a/app/views/projects/jobs/_user.html.haml
+++ b/app/views/projects/jobs/_user.html.haml
@@ -1,7 +1,7 @@
by
%a{ href: user_path(@build.user) }
- %span.hidden-xs
+ %span.d-none.d-sm-inline
= image_tag avatar_icon_for_user(@build.user, 24), class: "avatar s24"
%strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
= @build.user.name
- %strong.visible-xs-inline= @build.user.to_reference
+ %strong.d-inline.d-sm-none= @build.user.to_reference
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index 9963cc93633..fe1c338b634 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -15,7 +15,7 @@
- unless @repository.gitlab_ci_yml
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
- = link_to ci_lint_path, class: 'btn btn-default' do
+ = link_to project_ci_lint_path(@project), class: 'btn btn-default' do
%span CI lint
.content-list.builds-content-list
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index cbbcc8f1db5..ec9a04c0eab 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -15,7 +15,7 @@
- elsif @build.tags.any?
This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- @build.tags.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
- else
This job is stuck, because you don't have any active runners that can run this job.
@@ -58,13 +58,13 @@
- if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
.top-bar.js-top-bar
- .js-truncated-info.truncated-info.hidden-xs.pull-left.hidden<
+ .js-truncated-info.truncated-info.d-none.d-sm-block.float-left.hidden<
Showing last
%span.js-truncated-info-size.truncated-info-size><
of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
- .controllers.pull-right
+ .controllers.float-right
- if @build.has_trace?
= link_to raw_project_job_path(@project, @build),
title: 'Show complete raw',
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
index 243dcfdc187..0377cd6586e 100644
--- a/app/views/projects/mattermosts/_no_teams.html.haml
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -13,4 +13,4 @@
and try again.
%hr
.clearfix
- = link_to 'Go back', edit_project_service_path(@project, @service), class: 'btn btn-lg pull-right'
+ = link_to 'Go back', edit_project_service_path(@project, @service), class: 'btn btn-lg float-right'
diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index 20acd476f73..361d3c61d99 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -11,7 +11,7 @@
- options = options_for_select(mattermost_teams_options(@teams), selected_id)
= f.select(:team_id, options, { include_blank: 'Select team...'}, { class: 'form-control', disabled: @teams.one?, selected: selected_id, required: true })
= f.hidden_field(:team_id, value: selected_id, required: true) if @teams.one?
- .help-block
+ .form-text.text-muted
- if @teams.one?
This is the only available team.
- else
@@ -25,7 +25,7 @@
%h4 Command trigger word
%p Choose the word that will trigger commands
= f.text_field(:trigger, value: @project.path, class: 'form-control', required: true)
- .help-block
+ .form-text.text-muted
%p
Trigger word must be unique, and can't begin with a slash or contain any spaces.
Use the word that works best for your team.
@@ -41,6 +41,6 @@
= icon('external-link')
%hr
.clearfix
- .pull-right
+ .float-right
= link_to 'Cancel', edit_project_service_path(@project, @service), class: 'btn btn-lg'
= f.submit 'Install', class: 'btn btn-save btn-lg'
diff --git a/app/views/projects/mattermosts/new.html.haml b/app/views/projects/mattermosts/new.html.haml
index 15829a3f143..9e293d07cb7 100644
--- a/app/views/projects/mattermosts/new.html.haml
+++ b/app/views/projects/mattermosts/new.html.haml
@@ -1,7 +1,7 @@
- @body_class = 'card-content'
.service-installation
- .inline.pull-right
+ .inline.float-right
= custom_icon('mattermost_logo', size: 48)
%h3 Install Mattermost Command
- if @teams.empty?
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 9607a7b5d06..179c1fcc684 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,2 +1,2 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 54a661040ea..5353fa8a88f 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -1,9 +1,9 @@
-#modal_merge_info.modal
+#modal_merge_info.modal{ tabindex: '-1' }
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3 Check out, review, and merge locally
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
.modal-body
%p
%strong Step 1.
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index a94267deeb2..cd3d896fff2 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -9,21 +9,21 @@
%span.merge-request-title-text
= link_to merge_request.title, merge_request_path(merge_request)
- if merge_request.tasks?
- %span.task-status.hidden-xs
+ %span.task-status.d-none.d-sm-inline-block
&nbsp;
= merge_request.task_status
.issuable-info
%span.issuable-reference
#{issuable_reference(merge_request)}
- %span.issuable-authored.hidden-xs
+ %span.issuable-authored.d-none.d-sm-inline-block
&middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- if merge_request.milestone
- %span.issuable-milestone.hidden-xs
+ %span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
- = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do
+ = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
@@ -40,17 +40,17 @@
.issuable-meta
%ul.controls
- if merge_request.merged?
- %li.issuable-status.hidden-xs
+ %li.issuable-status.d-none.d-sm-inline-block
MERGED
- elsif merge_request.closed?
- %li.issuable-status.hidden-xs
+ %li.issuable-status.d-none.d-sm-inline-block
= icon('ban')
CLOSED
- if merge_request.head_pipeline
- %li.issuable-pipeline-status.hidden-xs
+ %li.issuable-pipeline-status.d-none.d-sm-inline-block
= render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken?
- %li.issuable-pipeline-broken.hidden-xs
+ %li.issuable-pipeline-broken.d-none.d-sm-inline-block
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
= icon('exclamation-triangle')
- if merge_request.assignee
@@ -59,5 +59,5 @@
= render 'shared/issuable_meta_data', issuable: merge_request
- .pull-right.issuable-updated-at.hidden-xs
+ .float-right.issuable-updated-at.d-none.d-sm-inline-block
%span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 22c8b6b513d..a58179091ae 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -7,8 +7,8 @@
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
- = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'hidden-sm hidden-md hidden-lg')
- %span.hidden-xs
+ = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'd-block d-sm-none')
+ %span.d-none.d-sm-block
= @merge_request.state_human_name
.issuable-meta
@@ -16,15 +16,15 @@
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request")
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ %button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ .dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
%ul
- if can_update_merge_request
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
@@ -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: "hidden-xs hidden-sm 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"
= render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
index 964dc40a213..e6205f24ae6 100644
--- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -11,6 +11,6 @@
Showing
%strong.cred {{conflictsCountText}}
between
- %strong {{conflictsData.sourceBranch}}
+ %strong.ref-name {{conflictsData.sourceBranch}}
and
- %strong {{conflictsData.targetBranch}}
+ %strong.ref-name {{conflictsData.targetBranch}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 13026b7566a..4d84ee2488b 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -1,16 +1,24 @@
-.form-horizontal.resolve-conflicts-form
- .form-group
- %label.col-sm-2.control-label{ "for" => "commit-message" }
- #{ _('Commit message') }
- .col-sm-10
+- branch_name = link_to @merge_request.source_branch, project_tree_path(@merge_request.project, @merge_request.source_branch), class: "ref-name"
+- translation =_('You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}') % { use_ours: '<code>Use Ours</code>', use_theirs: '<code>Use Theirs</code>', branch_name: branch_name }
+
+%hr
+.resolve-conflicts-form
+ .form-group.row
+ .col-md-4
+ %h4= _('Resolve conflicts on source branch')
+ .resolve-info
+ = translation.html_safe
+ .col-md-8
+ %label.label-light{ "for" => "commit-message" }
+ #{ _('Commit message') }
.commit-message-container
.max-width-marker
%textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" }
- .form-group
- .col-sm-offset-2.col-sm-10
+ .form-group.row
+ .offset-md-4.col-md-8
.row
- .col-xs-6
+ .col-6
%button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}}
- .col-xs-6.text-right
+ .col-6.text-right
= link_to "Cancel", project_merge_request_path(@merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index f81db9b4e28..ca0f7d6098f 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -1,17 +1,17 @@
%h3.page-title
New Merge Request
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
- .js-merge-request-new-compare.row{ 'data-target-project-url': project_new_merge_request_update_branches_path(@source_project), 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
- .col-md-6
- .panel.panel-default.panel-new-merge-request
- .panel-heading
+ .js-merge-request-new-compare.row{ 'data-source-branch-url': project_new_merge_request_branch_from_path(@source_project), 'data-target-branch-url': project_new_merge_request_branch_to_path(@source_project) }
+ .col-lg-6
+ .card.card-new-merge-request
+ .card-header
Source branch
- .panel-body.clearfix
+ .card-body.clearfix
.merge-request-select.dropdown
= f.hidden_field :source_project_id
- = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", field_name: "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
+ = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
= dropdown_title("Select source project")
= dropdown_filter("Search projects")
@@ -21,27 +21,25 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch.git-revision-dropdown
- = dropdown_title("Select source branch")
- = dropdown_filter("Search branches")
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/branch',
- branches: @merge_request.source_branches,
- selected: f.object.source_branch
+ = dropdown_toggle f.object.source_branch || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch }, { toggle_class: "js-compare-dropdown js-source-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
+ = dropdown_title(_("Select source branch"))
+ = dropdown_filter(_("Search branches"))
+ = dropdown_content
+ = dropdown_loading
.panel-footer
.text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
- .col-md-6
- .panel.panel-default.panel-new-merge-request
- .panel-heading
+ .col-lg-6
+ .card.card-new-merge-request
+ .card-header
Target branch
- .panel-body.clearfix
+ .card-body.clearfix
- projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
- = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
+ = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-project
= dropdown_title("Select target project")
= dropdown_filter("Search projects")
@@ -51,15 +49,13 @@
selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
- = dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown.git-revision-dropdown
- = dropdown_title("Select target branch")
- = dropdown_filter("Search branches")
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/branch',
- branches: @merge_request.target_branches,
- selected: f.object.target_branch
- .panel-footer
+ = dropdown_toggle f.object.target_branch, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch git-revision-dropdown-toggle" }
+ .dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
+ = dropdown_title(_("Select target branch"))
+ = dropdown_filter(_("Search branches"))
+ = dropdown_content
+ = dropdown_loading
+ .card-footer
.text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index 68780cedeb1..ebcd99f2a9b 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -7,10 +7,10 @@
%span into
%strong.ref-name= target_title
- %span.pull-right
+ %span.float-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
@@ -24,20 +24,20 @@
There are no commits yet.
= custom_icon ('illustration_no_commits')
- else
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom
%li.commits-tab.active
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
- %span.badge= @commits.size
+ %span.badge.badge-pill= @commits.size
- if @pipelines.any?
%li.builds-tab
= link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
- %span.badge= @pipelines.size
+ %span.badge.badge-pill= @pipelines.size
%li.diffs-tab
= link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
- %span.badge= @merge_request.diff_size
+ %span.badge.badge-pill= @merge_request.diff_size
.tab-content
#commits.commits.tab-pane.active
diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
index 2e5594f8cbe..dab95b97346 100644
--- a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
+++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
@@ -1,5 +1,5 @@
- if @commit
- .info-well.hidden-xs.prepend-top-default
+ .info-well.d-none.d-sm-block.prepend-top-default
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true
diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index 986ba5ae02d..19659fe5140 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -5,9 +5,9 @@
- if @merge_request_diff&.empty?
.row.empty-state.nothing-here-block
- .col-xs-12
+ .col-12
.svg-content= image_tag 'illustrations/merge_request_changes_empty.svg'
- .col-xs-12
+ .col-12
.text-content.text-center
%p
No changes between
diff --git a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
index 529fbb8547a..8d7138747fb 100644
--- a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
+++ b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
@@ -11,7 +11,7 @@
comparing two versions of the diff
- else
viewing an old version of the diff
- .pull-right
+ .float-right
= link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do
Show latest version
= "of the diff" if @commit
diff --git a/app/views/projects/merge_requests/dropdowns/_project.html.haml b/app/views/projects/merge_requests/dropdowns/_project.html.haml
index aaf1ab00eeb..b3cf3c1d369 100644
--- a/app/views/projects/merge_requests/dropdowns/_project.html.haml
+++ b/app/views/projects/merge_requests/dropdowns/_project.html.haml
@@ -1,5 +1,5 @@
%ul
- projects.each do |project|
%li
- %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id } }
+ %a{ href: "#", class: "#{('is-active' if selected == project.id)}", data: { id: project.id, 'refs-url': refs_project_path(project) } }
= project.full_path
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index 6df19d6438b..749228a9664 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -10,13 +10,13 @@
- if @merge_request.for_fork? && !@merge_request.source_project
fork project was removed
- elsif !@merge_request.source_branch_exists?
- %span.label.label-inverse= @merge_request.source_branch
+ %span.badge.badge-inverse= @merge_request.source_branch
does not exist in
- %span.label.label-info= @merge_request.source_project_path
+ %span.badge.badge-info= @merge_request.source_project_path
- elsif !@merge_request.target_branch_exists?
- %span.label.label-inverse= @merge_request.target_branch
+ %span.badge.badge-inverse= @merge_request.target_branch
does not exist in
- %span.label.label-info= @merge_request.target_project_path
+ %span.badge.badge-info= @merge_request.target_project_path
- else
of internal error
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 15a0e4d7ef5..aa3fb623e58 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -30,26 +30,26 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- .nav-links.scrolling-tabs
- %ul.merge-request-tabs
+ .nav-links.scrolling-tabs.nav.nav-tabs
+ %ul.merge-request-tabs.nav-tabs.nav
%li.notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion
- %span.badge= @merge_request.related_notes.user.count
+ %span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= tab_link_for @merge_request, :commits do
Commits
- %span.badge= @commits_count
+ %span.badge.badge-pill= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= tab_link_for @merge_request, :pipelines do
Pipelines
- %span.badge.js-pipelines-mr-count= @pipelines.size
+ %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab
= tab_link_for @merge_request, :diffs do
Changes
- %span.badge= @merge_request.diff_size
+ %span.badge.badge-pill= @merge_request.diff_size
- if has_vue_discussions_cookie?
#js-vue-discussion-counter
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 2e74b1b83cb..4cc59718715 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,13 +1,13 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
= form_errors(@milestone)
.row
.col-md-6
- .form-group
- = f.label :title, "Title", class: "control-label"
+ .form-group.row
+ = f.label :title, "Title", class: "col-form-label col-sm-2"
.col-sm-10
= f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true
- .form-group.milestone-description
- = f.label :description, "Description", class: "control-label"
+ .form-group.row.milestone-description
+ = f.label :description, "Description", class: "col-form-label col-sm-2"
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...'
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 5ec219fdf00..b478fbbb15e 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -56,7 +56,7 @@
#delete-milestone-modal
- %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.detail-page-description.milestone-detail
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
new file mode 100644
index 00000000000..64f0fde30cf
--- /dev/null
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -0,0 +1,10 @@
+.account-well.prepend-top-default.append-bottom-default
+ %ul
+ %li
+ The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>.
+ %li
+ Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.
+ %li
+ The update action will time out after 10 minutes. For big repositories, use a clone/push combination.
+ %li
+ The Git LFS objects will <strong>not</strong> be synced.
diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml
new file mode 100644
index 00000000000..4a6aefce351
--- /dev/null
+++ b/app/views/projects/mirrors/_push.html.haml
@@ -0,0 +1,50 @@
+- expanded = Rails.env.test?
+%section.settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ Push to a remote repository
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Set up the remote repository that you want to update with the content of the current repository
+ every time someone pushes to it.
+ = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
+ .settings-content
+ = form_for @project, url: project_mirror_path(@project) do |f|
+ %div
+ = form_errors(@project)
+ = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
+ - if @remote_mirror.last_error.present?
+ .panel.panel-danger
+ .panel-heading
+ - if @remote_mirror.last_update_at
+ The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}.
+ - else
+ The remote repository failed to update.
+
+ - if @remote_mirror.last_successful_update_at
+ Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
+ .panel-body
+ %pre
+ :preserve
+ #{h(@remote_mirror.last_error.strip)}
+ = f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
+ .form-group
+ = rm_form.check_box :enabled, class: "pull-left"
+ .prepend-left-20
+ = rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
+ %p.light.append-bottom-0
+ Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
+ .form-group.has-feedback
+ = rm_form.label :url, "Git repository URL", class: "label-light"
+ = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+
+ = render "projects/mirrors/instructions"
+
+ .form-group
+ = rm_form.check_box :only_protected_branches, class: 'pull-left'
+ .prepend-left-20
+ = rm_form.label :only_protected_branches, class: 'label-light'
+ = link_to icon('question-circle'), help_page_path('user/project/protected_branches')
+
+ = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
diff --git a/app/views/projects/mirrors/_show.html.haml b/app/views/projects/mirrors/_show.html.haml
new file mode 100644
index 00000000000..de77701a373
--- /dev/null
+++ b/app/views/projects/mirrors/_show.html.haml
@@ -0,0 +1,3 @@
+- if can?(current_user, :admin_remote_mirror, @project)
+ = render 'projects/mirrors/push'
+
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 4b7be9a223f..75cb8245d36 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -9,7 +9,7 @@
= button_tag class: 'btn btn-success' do
= icon('search')
.inline.prepend-left-20
- .checkbox.light
+ .form-check.light
= label_tag :filter_ref do
= check_box_tag :filter_ref, 1, @options[:filter_ref]
%span= _("Begin with the selected commit")
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index b66e0559603..35a09f06bfa 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -28,19 +28,19 @@
%template.push-new-project-tip-template= render partial: "new_project_push_tip"
.col-lg-9.js-toggle-container
- %ul.nav-links.gitlab-tabs{ role: 'tablist' }
+ %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
%li{ class: active_when(active_tab == 'blank'), role: 'presentation' }
%a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
- %span.hidden-xs Blank project
- %span.visible-xs Blank
+ %span.d-none.d-sm-block Blank project
+ %span.d-block.d-sm-none Blank
%li{ class: active_when(active_tab == 'template'), role: 'presentation' }
%a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
- %span.hidden-xs Create from template
- %span.visible-xs Template
+ %span.d-none.d-sm-block Create from template
+ %span.d-block.d-sm-none Template
%li{ class: active_when(active_tab == 'import'), role: 'presentation' }
%a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
- %span.hidden-xs Import project
- %span.visible-xs Import
+ %span.d-none.d-sm-block Import project
+ %span.d-block.d-sm-none Import
.tab-content.gitlab-tab-content
.tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
@@ -57,56 +57,13 @@
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled?
- .project-import.row
- .col-lg-12
- .form-group.import-btn-container.clearfix
- = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
- Import project from
- .import-buttons
- - if gitlab_project_import_enabled?
- .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
- = icon('gitlab', text: 'GitLab export')
- %div
- - if github_import_enabled?
- = link_to new_import_github_path, class: 'btn js-import-github' do
- = icon('github', text: 'GitHub')
- %div
- - if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- = icon('bitbucket', text: 'Bitbucket')
- - unless bitbucket_import_configured?
- = render 'bitbucket_import_modal'
- %div
- - if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
- = icon('gitlab', text: 'GitLab.com')
- - unless gitlab_import_configured?
- = render 'gitlab_import_modal'
- %div
- - if google_code_import_enabled?
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
- = icon('google', text: 'Google Code')
- %div
- - if fogbugz_import_enabled?
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- = icon('bug', text: 'Fogbugz')
- %div
- - if gitea_import_enabled?
- = link_to new_import_gitea_path, class: 'btn import_gitea' do
- = custom_icon('go_logo')
- Gitea
- %div
- - if git_import_enabled?
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
- = icon('git', text: 'Repo by URL')
- .col-lg-12
- .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
- %hr
- = render "shared/import_form", f: f
- = render 'new_project_fields', f: f, project_name_id: "import-url-name"
+ = render 'import_project_pane', f: f, active_tab: active_tab
+ - else
+ .nothing-here-block
+ %h4 No import options available
+ %p Contact an administrator to enable options for importing your project.
-.save-project-loader.hide
+.save-project-loader.d-none
.center
%h2
%i.fa.fa-spinner.fa-spin
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index 14d880028c7..08772a0188b 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -21,4 +21,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
+ = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 82e20eeebb3..73ea30e1d3d 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -1,8 +1,8 @@
- if @project.pages_deployed?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Access pages
- .panel-body
+ .card-body
%p
%strong
Congratulations! Your pages are served under:
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
index 7d6c30b7f8d..4ada19a1368 100644
--- a/app/views/projects/pages/_destroy.haml
+++ b/app/views/projects/pages/_destroy.haml
@@ -1,9 +1,9 @@
- if @project.pages_deployed?
- if can?(current_user, :remove_pages, @project)
- .panel.panel-default.panel.panel-danger
- .panel-heading Remove pages
+ .card.bg-danger
+ .card-header Remove pages
.errors-holder
- .panel-body
+ .card-body
%p
Removing the pages will prevent from exposing them to outside world.
.form-actions
diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml
index 6a3ffce949f..57345edb90b 100644
--- a/app/views/projects/pages/_https_only.html.haml
+++ b/app/views/projects/pages/_https_only.html.haml
@@ -1,5 +1,5 @@
= 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: 'pull-left', disabled: pages_https_only_disabled?
+ = 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
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 27bbe52a714..986ca852411 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -1,8 +1,8 @@
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- if can?(current_user, :update_pages, @project) && @domains.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Domains (#{@domains.count})
%ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
@@ -17,9 +17,9 @@
= icon('external-link')
- if domain.subject
%p
- %span.label.label-gray Certificate: #{domain.subject}
+ %span.badge.badge-gray Certificate: #{domain.subject}
- if domain.expired?
- %span.label.label-danger Expired
+ %span.badge.badge-danger Expired
%div
= 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"
diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml
index 7cea5f3e70b..8c93cf7a8ad 100644
--- a/app/views/projects/pages/_no_domains.html.haml
+++ b/app/views/projects/pages/_no_domains.html.haml
@@ -1,6 +1,6 @@
- if can?(current_user, :update_pages, @project)
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Domains
.nothing-here-block
Support for domains and certificates is disabled.
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
index e442e6e9a09..cd9177c0f9e 100644
--- a/app/views/projects/pages/_use.html.haml
+++ b/app/views/projects/pages/_use.html.haml
@@ -1,8 +1,8 @@
- unless @project.pages_deployed?
- .panel.panel-info
- .panel-heading
+ .card.bg-info
+ .card-header
Configure pages
- .panel-body
+ .card-body
%p
Learn how to upload your static site and have it served by
GitLab by following the
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 6adaea799b2..7e1a3b9bea6 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -4,7 +4,7 @@
Pages
- if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
- = link_to new_project_pages_domain_path(@project), class: 'btn btn-new pull-right', title: 'New Domain' do
+ = link_to new_project_pages_domain_path(@project), class: 'btn btn-new float-right', title: 'New Domain' do
New Domain
%p.light
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index d81b07832bb..0d848f7899c 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -4,22 +4,22 @@
- @domain.errors.full_messages.each do |msg|
%p= msg
-.form-group
- = f.label :domain, class: 'control-label' do
+.form-group.row
+ = f.label :domain, class: 'col-form-label col-sm-2' do
Domain
.col-sm-10
= f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
- if Gitlab.config.pages.external_https
- .form-group
- = f.label :certificate, class: 'control-label' do
+ .form-group.row
+ = f.label :certificate, class: 'col-form-label col-sm-2' do
Certificate (PEM)
.col-sm-10
= f.text_area :certificate, rows: 5, class: 'form-control'
%span.help-inline Upload a certificate for your domain with all intermediates
- .form-group
- = f.label :key, class: 'control-label' do
+ .form-group.row
+ = f.label :key, class: 'col-form-label col-sm-2' do
Key (PEM)
.col-sm-10
= f.text_area :key, rows: 5, class: 'form-control'
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index 6c404990492..ee70de22f13 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -5,7 +5,7 @@
= @domain.domain
%hr.clearfix
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
= f.submit 'Save Changes', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 269df803a2b..376ce3f68aa 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -4,9 +4,9 @@
New Pages Domain
%hr.clearfix
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
= f.submit 'Create New Domain', class: "btn btn-save"
- .pull-right
+ .float-right
= link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index 44d66f3b2d0..a8484187493 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -12,7 +12,7 @@
This domain is not verified. You will need to verify ownership before access is enabled.
%h3.page-title.with-button
- = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right'
+ = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right'
Pages Domain
.table-holder
@@ -30,9 +30,9 @@
%td
.input-group
= text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#domain_dns', class: 'btn-default hidden-xs')
- %p.help-block
+ .input-group-append
+ = clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
+ %p.form-text.text-muted
To access this domain create a new DNS record
- if verification_enabled
@@ -43,16 +43,16 @@
%td
= form_tag verify_project_pages_domain_path(@project, @domain) do
.status-badge
- - text, status = @domain.unverified? ? [_('Unverified'), 'label-danger'] : [_('Verified'), 'label-success']
- .label{ class: status }
+ - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
+ .badge{ class: status }
= text
%button.btn.has-tooltip{ type: "submit", data: { container: 'body' }, title: _("Retry verification") }
= sprite_icon('redo')
.input-group
= text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-btn
- = clipboard_button(target: '#domain_verification', class: 'btn-default hidden-xs')
- %p.help-block
+ .input-group-append
+ = clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
+ %p.form-text.text-muted
- help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
To #{link_to 'verify ownership', help_link} of your domain,
add the above key to a TXT record within to your DNS configuration.
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 160e325996a..1cdf981fcb4 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -1,24 +1,24 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
- .form-group
+ .form-group.row
.col-md-9
= f.label :description, _('Description'), class: 'label-light'
= f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: s_('PipelineSchedules|Provide a short description for this pipeline')
- .form-group
+ .form-group.row
.col-md-9
= f.label :cron, _('Interval Pattern'), class: 'label-light'
#interval-pattern-input{ data: { initial_interval: @schedule.cron } }
- .form-group
+ .form-group.row
.col-md-9
= f.label :cron_timezone, _('Cron Timezone'), class: 'label-light'
= dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
= f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true
- .form-group
+ .form-group.row
.col-md-9
= f.label :ref, _('Target Branch'), class: 'label-light'
= dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } )
= f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true
- .form-group.js-ci-variable-list-section
+ .form-group.row.js-ci-variable-list-section
.col-md-9
%label.label-light
#{ s_('PipelineSchedules|Variables') }
@@ -32,7 +32,7 @@
= n_('Hide value', 'Hide values', @schedule.variables.size)
- else
= n_('Reveal value', 'Reveal values', @schedule.variables.size)
- .form-group
+ .form-group.row
.col-md-9
= f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light'
%div
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 55d0e8bb7f9..8d88f0be083 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -25,7 +25,7 @@
= link_to user_path(pipeline_schedule.owner) do
= pipeline_schedule.owner&.name
%td
- .pull-right.btn-group
+ .float-right.btn-group
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
= link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn' do
= icon('play')
diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml
index 8996c1b3e38..61f6ad34052 100644
--- a/app/views/projects/pipeline_schedules/_tabs.html.haml
+++ b/app/views/projects/pipeline_schedules/_tabs.html.haml
@@ -1,18 +1,18 @@
-%ul.nav-links.mobile-separator
+%ul.nav-links.mobile-separator.nav.nav-tabs
%li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All")
- %span.badge.js-totalbuilds-count
+ %span.badge.badge-pill.js-totalbuilds-count
= number_with_delimiter(all_schedules.count(:id))
%li{ class: active_when(scope == 'active') }>
= link_to schedule_path_proc.call('active') do
= s_("PipelineSchedules|Active")
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(all_schedules.active.count(:id))
%li{ class: active_when(scope == 'inactive') }>
= link_to schedule_path_proc.call('inactive') do
= s_("PipelineSchedules|Inactive")
- %span.badge
+ %span.badge.badge-pill
= number_with_delimiter(all_schedules.inactive.count(:id))
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index bcb6dddba1a..3677666070e 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -18,5 +18,5 @@
%ul.content-list
= render partial: "table"
- else
- .light-well
+ .card.bg-light
.nothing-here-block= _("No schedules")
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 85946aec1f2..aa53fc3ea28 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -5,7 +5,7 @@
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
- %pre.commit-description
+ .commit-description<
= preserve(markdown(@commit.description, pipeline: :single_line))
.info-well
@@ -27,7 +27,7 @@
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ = link_to("#", class: "js-details-expand d-none d-sm-none d-md-inline") do
%span.text-expander
\...
%span.js-details-content.hide
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 852143ecb2a..118391aac64 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,19 +1,17 @@
-- failed_builds = @pipeline.statuses.latest.failed
-
.tabs-holder
- %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator
+ %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link
= link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
= _("Pipeline")
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _("Jobs")
- %span.badge.js-builds-counter= pipeline.total_size
- - if failed_builds.present?
+ %span.badge.badge-pill.js-builds-counter= pipeline.total_size
+ - if @pipeline.failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _("Failed Jobs")
- %span.badge.js-failures-counter= failed_builds.count
+ %span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count
.tab-content
#js-tab-pipeline.tab-pane
@@ -26,7 +24,7 @@
%ul
- pipeline.yaml_errors.split(",").each do |error|
%li= error
- You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
.bs-callout.bs-callout-warning
@@ -43,9 +41,10 @@
%th Coverage
%th
= render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- - if failed_builds.present?
+
+ - if @pipeline.failed_builds.present?
#js-tab-failures.build-failures.tab-pane
- - failed_builds.each_with_index do |build, index|
+ - @pipeline.failed_builds.each_with_index do |build, index|
.build-state
%span.ci-status-icon-failed= custom_icon('icon_status_failed')
%span.stage
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 8f2142af2ce..d1e8e9d0d60 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -1,24 +1,34 @@
- breadcrumb_title "Pipelines"
- page_title = s_("Pipeline|Run Pipeline")
+- settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project)
%h3.page-title
= s_("Pipeline|Run Pipeline")
%hr
-= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f|
+= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
= form_errors(@pipeline)
- .form-group
- = f.label :ref, s_('Pipeline|Run on'), class: 'control-label'
- .col-sm-10
+ .form-group.row
+ .col-sm-12
+ = f.label :ref, s_('Pipeline|Create for')
= hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle',
filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .help-block
- = s_("Pipeline|Existing branch name, tag")
+ .form-text.text-muted
+ = s_("Pipeline|Existing branch name or tag")
+
+ .col-sm-12.prepend-top-10.js-ci-variable-list-section
+ %label
+ = s_('Pipeline|Variables')
+ %ul.ci-variable-list
+ = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
+ .form-text.text-muted
+ = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
+
.form-actions
- = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3
+ = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index fdeb5f21fbe..128f52ff648 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -1,7 +1,7 @@
-.panel.panel-default.project-members-groups
- .panel-heading
+.card.project-members-groups
+ .card-header
Groups with access to
%strong= @project.name
- %span.badge= group_links.size
+ %span.badge.badge-pill= group_links.size
%ul.content-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index bf5b11ea30c..5064db8d5cd 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -9,7 +9,7 @@
.select-wrapper
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control"
= icon('chevron-down')
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
.form-group
diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_shared_group.html.haml
index c10ef648a8f..684219735e2 100644
--- a/app/views/projects/project_members/_new_shared_group.html.haml
+++ b/app/views/projects/project_members/_new_shared_group.html.haml
@@ -9,7 +9,7 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
= icon('chevron-down')
- .help-block.append-bottom-10
+ .form-text.text-muted.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
.form-group
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 16bcf671c25..a30870a241c 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,12 +1,12 @@
- project = local_assigns.fetch(:project)
- members = local_assigns.fetch(:members)
-.panel.panel-default
- .panel-heading.flex-project-members-panel
+.card
+ .card-header.flex-project-members-panel
%span.flex-project-title
Members of
%strong= project.name
- %span.badge= members.total_count
+ %span.badge.badge-pill= members.total_count
= form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 755128af565..6a52e72bfd8 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -5,9 +5,9 @@
%p.light
Only project members will be imported. Group members will be skipped.
%hr
-= form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do
- .form-group
- = label_tag :source_project_id, "Project", class: 'control-label'
+= form_tag apply_import_project_project_members_path(@project), method: 'post' do
+ .form-group.row
+ = label_tag :source_project_id, "Project", class: 'col-form-label col-sm-2'
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index d81103c3a92..a56023e98cd 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -17,7 +17,7 @@
%i Owners
.light
- if can?(current_user, :admin_project_member, @project)
- %ul.nav-links.gitlab-tabs{ role: 'tablist' }
+ %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
%a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
- if @project.allowed_to_share_with_group?
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 5377d745371..24d2b971472 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -1,4 +1,4 @@
- can_admin_project = can?(current_user, :admin_project, @project)
= render layout: 'projects/protected_branches/shared/branches_list', locals: { can_admin_project: can_admin_project } do
- = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
+ = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index 12ccae10260..24b53555cdc 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -1,8 +1,8 @@
- content_for :merge_access_levels do
.merge_access_levels-container
= dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-merge wide',
- dropdown_class: 'dropdown-menu-selectable capitalize-header',
+ options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge-select wide',
+ dropdown_class: 'dropdown-menu-selectable qa-allowed-to-merge-dropdown capitalize-header',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
- content_for :push_access_levels do
.push_access_levels-container
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
index 98363f2018a..f242459f69b 100644
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -1,7 +1,7 @@
%td
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
= dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
+ options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
%td
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index d1ed438eb21..a2cd7752fc4 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,7 +1,7 @@
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
- .panel-heading
- %h3.panel-title
+ .card-header
+ %h3.card-title
Protected branch (#{@protected_branches_count})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
index 9f0c4f3b3a8..c2d6c034e35 100644
--- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
@@ -1,33 +1,32 @@
= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f|
- .panel.panel-default
- .panel-heading
- %h3.panel-title
+ .card
+ .card-header
+ %h3.card-title
Protect a branch
- .panel-body
- .form-horizontal
- = form_errors(@protected_branch)
- .form-group
- = f.label :name, class: 'col-md-2 text-right' do
- Branch:
- .col-md-10
- = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f }
- .help-block
- = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
- such as
- %code *-stable
- or
- %code production/*
- are supported
- .form-group
- %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
- Allowed to merge:
- .col-md-10
- = yield :merge_access_levels
- .form-group
- %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
- Allowed to push:
- .col-md-10
- = yield :push_access_levels
+ .card-body
+ = form_errors(@protected_branch)
+ .form-group.row
+ = f.label :name, class: 'col-md-2 text-right' do
+ Branch:
+ .col-md-10
+ = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f }
+ .form-text.text-muted
+ = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
+ such as
+ %code *-stable
+ or
+ %code production/*
+ are supported
+ .form-group.row
+ %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
+ Allowed to merge:
+ .col-md-10
+ = yield :merge_access_levels
+ .form-group.row
+ %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
+ Allowed to push:
+ .col-md-10
+ = yield :push_access_levels
- .panel-footer
+ .card-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/protected_branches/shared/_matching_branch.html.haml b/app/views/projects/protected_branches/shared/_matching_branch.html.haml
index 98793d632e6..2c76bf87945 100644
--- a/app/views/projects/protected_branches/shared/_matching_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_matching_branch.html.haml
@@ -3,7 +3,7 @@
= link_to matching_branch.name, project_ref_path(@project, matching_branch.name), class: 'ref-name'
- if @project.root_ref?(matching_branch.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- commit = @project.commit(matching_branch.name)
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index f5b21f0e887..82ef08272d3 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -5,7 +5,7 @@
%span.ref-name.qa-protected-branch-name= protected_branch.name
- if @project.root_ref?(protected_branch.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- if protected_branch.wildcard?
- matching_branches = protected_branch.matching(repository.branches)
@@ -21,4 +21,4 @@
- if can_admin_project
%td
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
+ = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], disabled: local_assigns[:disabled], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning"
diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
index 5a53c704fcb..0ae5ca3ff36 100644
--- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
@@ -1,29 +1,28 @@
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
- .panel.panel-default
- .panel-heading
- %h3.panel-title
+ .card
+ .card-header
+ %h3.card-title
Protect a tag
- .panel-body
- .form-horizontal
- = form_errors(@protected_tag)
- .form-group
- = f.label :name, class: 'col-md-2 text-right' do
- Tag:
- .col-md-10.protected-tags-dropdown
- = render partial: "projects/protected_tags/shared/dropdown", locals: { f: f }
- .help-block
- = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
- such as
- %code v*
- or
- %code *-release
- are supported
- .form-group
- %label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
- Allowed to create:
- .col-md-10
- .create_access_levels-container
- = yield :create_access_levels
+ .card-body
+ = form_errors(@protected_tag)
+ .form-group.row
+ = f.label :name, class: 'col-md-2 text-right' do
+ Tag:
+ .col-md-10.protected-tags-dropdown
+ = render partial: "projects/protected_tags/shared/dropdown", locals: { f: f }
+ .form-text.text-muted
+ = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
+ such as
+ %code v*
+ or
+ %code *-release
+ are supported
+ .form-group.row
+ %label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
+ Allowed to create:
+ .col-md-10
+ .create_access_levels-container
+ = yield :create_access_levels
- .panel-footer
+ .card-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/protected_tags/shared/_matching_tag.html.haml b/app/views/projects/protected_tags/shared/_matching_tag.html.haml
index 05f102d1ca3..133c76cd2ad 100644
--- a/app/views/projects/protected_tags/shared/_matching_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_matching_tag.html.haml
@@ -3,7 +3,7 @@
= link_to matching_tag.name, project_ref_path(@project, matching_tag.name), class: 'ref-name'
- if @project.root_ref?(matching_tag.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- commit = @project.commit(matching_tag.name)
= link_to(commit.short_id, project_commit_path(@project, commit.id), class: 'commit-sha')
diff --git a/app/views/projects/protected_tags/shared/_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_protected_tag.html.haml
index c778f7b9781..1702e38df7e 100644
--- a/app/views/projects/protected_tags/shared/_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_protected_tag.html.haml
@@ -3,7 +3,7 @@
%span.ref-name= protected_tag.name
- if @project.root_ref?(protected_tag.name)
- %span.label.label-info.prepend-left-5 default
+ %span.badge.badge-info.prepend-left-5 default
%td
- if protected_tag.wildcard?
- matching_tags = protected_tag.matching(repository.tags)
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 3ed82e51dbe..c3081d75fb4 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -1,7 +1,7 @@
.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
- .panel-heading
- %h3.panel-title
+ .card-header
+ %h3.card-title
Protected tag (#{@protected_tags_count})
%p.settings-message.text-center
There are currently no protected tags, protect a tag with the form above.
diff --git a/app/views/projects/registry/repositories/_tag.html.haml b/app/views/projects/registry/repositories/_tag.html.haml
index 0b082a2137f..a4cde53e8c6 100644
--- a/app/views/projects/registry/repositories/_tag.html.haml
+++ b/app/views/projects/registry/repositories/_tag.html.haml
@@ -18,13 +18,13 @@
\-
%td
- if tag.created_at
- = time_ago_in_words(tag.created_at)
+ = time_ago_with_tooltip tag.created_at
- else
.light
\-
- if can?(current_user, :update_container_image, @project)
%td.content
- .controls.hidden-xs.pull-right
+ .controls.d-none.d-sm-block.float-right
= link_to project_registry_repository_tag_path(@project, tag.repository, tag.name),
method: :delete,
class: 'btn btn-remove has-tooltip',
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 2c80f7c3fa3..0426f2215ad 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -16,11 +16,11 @@
.row.prepend-top-10
.col-lg-12
- .panel.panel-default
- .panel-heading
- %h4.panel-title
+ .card
+ .card-header
+ %h4.card-title
= s_('ContainerRegistry|How to use the Container Registry')
- .panel-body
+ .card-body
%p
- link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
- link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
@@ -29,8 +29,8 @@
docker login #{Gitlab.config.registry.host_port}
%br
%p
- - deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
- = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
+ - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
+ = s_('ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br
%p
= s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 4d962f9433f..b4787032966 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -11,7 +11,7 @@
%strong= @tag.name
- = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
+ = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'shared/notes/hints'
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index 87895a15239..ae0d9ab9908 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -14,5 +14,5 @@
= image_tag avatar_icon_for_email(commit.author_email), class: "", width: 16, alt: ''
= markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
%td
- %span.pull-right.cgray
+ %span.float-right.cgray
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
deleted file mode 100644
index 6a681736b6f..00000000000
--- a/app/views/projects/runners/_form.html.haml
+++ /dev/null
@@ -1,55 +0,0 @@
-= form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f|
- = form_errors(runner)
- .form-group
- = label :active, "Active", class: 'control-label'
- .col-sm-10
- .checkbox
- = f.check_box :active
- %span.light Paused Runners don't accept new jobs
- .form-group
- = label :protected, "Protected", class: 'control-label'
- .col-sm-10
- .checkbox
- = f.check_box :access_level, {}, 'ref_protected', 'not_protected'
- %span.light This runner will only run on pipelines triggered on protected branches
- .form-group
- = label :run_untagged, 'Run untagged jobs', class: 'control-label'
- .col-sm-10
- .checkbox
- = f.check_box :run_untagged
- %span.light Indicates whether this runner can pick jobs without tags
- .form-group
- = label :locked, 'Lock to current projects', class: 'control-label'
- .col-sm-10
- .checkbox
- = f.check_box :locked
- %span.light When a runner is locked, it cannot be assigned to other projects
- .form-group
- = label_tag :token, class: 'control-label' do
- Token
- .col-sm-10
- = f.text_field :token, class: 'form-control', readonly: true
- .form-group
- = label_tag :ip_address, class: 'control-label' do
- IP Address
- .col-sm-10
- = f.text_field :ip_address, class: 'form-control', readonly: true
- .form-group
- = label_tag :description, class: 'control-label' do
- Description
- .col-sm-10
- = f.text_field :description, class: 'form-control'
- .form-group
- = label_tag :maximum_timeout_human_readable, class: 'control-label' do
- Maximum job timeout
- .col-sm-10
- = f.text_field :maximum_timeout_human_readable, class: 'form-control'
- .help-block This timeout will take precedence when lower than Project-defined timeout
- .form-group
- = label_tag :tag_list, class: 'control-label' do
- Tags
- .col-sm-10
- = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
- .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas.
- .form-actions
- = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
new file mode 100644
index 00000000000..dfed0553f84
--- /dev/null
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -0,0 +1,37 @@
+- link = link_to _('Runners API'), help_page_path('api/runners.md')
+
+%h3
+ = _('Group Runners')
+
+.bs-callout.bs-callout-warning
+ = _('GitLab Group Runners can execute code for all the projects in this group.')
+ = _('They can be managed using the %{link}.').html_safe % { link: link }
+
+ - if @project.group
+ %hr
+ - if @project.group_runners_enabled?
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
+ = _('Disable group Runners')
+ - else
+ = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success btn-inverted', method: :post do
+ = _('Enable group Runners')
+ &nbsp;
+ = _('for this project')
+
+- if !@project.group
+ = _('This project does not belong to a group and can therefore not make use of group Runners.')
+
+- elsif @group_runners.empty?
+ = _('This group does not provide any group Runners yet.')
+
+ - if can?(current_user, :admin_pipeline, @project.group)
+ - group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group)
+ = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link }
+ - else
+ = _('Ask your group master to setup a group Runner.')
+
+- else
+ %h4.underlined-title
+ = _('Available group Runners : %{runners}').html_safe % { runners: @group_runners.count }
+ %ul.bordered-list
+ = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml
index f9808f7c990..022687b831f 100644
--- a/app/views/projects/runners/_index.html.haml
+++ b/app/views/projects/runners/_index.html.haml
@@ -1,19 +1,4 @@
-.light.prepend-top-default
- %p
- A 'Runner' is a process which runs a job.
- You can setup as many Runners as you need.
- %br
- Runners can be placed on separate users, servers, and even on your local machine.
-
- %p Each Runner can be in one of the following states:
- %div
- %ul
- %li
- %span.label.label-success active
- \- Runner is active and can process any new jobs
- %li
- %span.label.label-danger paused
- \- Runner is paused and will not receive any new jobs
+= render 'shared/runners/runner_description'
%hr
@@ -23,3 +8,4 @@
= render 'projects/runners/specific_runners'
.col-sm-6
= render 'projects/runners/shared_runners'
+ = render 'projects/runners/group_runners'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 6376496ee1a..a23f5d6f0c3 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -3,10 +3,10 @@
= runner_status_icon(runner)
- if @project_runners.include?(runner)
- = link_to runner.short_sha, runner_path(runner), class: 'commit-sha'
+ = link_to runner.short_sha, project_runner_path(@project, runner), class: 'commit-sha'
- if runner.locked?
- = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
+ = icon('lock', class: 'has-tooltip', title: _('Locked to current projects'))
%small.edit-runner
= link_to edit_project_runner_path(@project, runner) do
@@ -15,22 +15,22 @@
%span.commit-sha
= runner.short_sha
- .pull-right
+ .float-right
- if @project_runners.include?(runner)
- if runner.active?
- = link_to 'Pause', pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: "Are you sure?" }
+ = link_to _('Pause'), pause_project_runner_path(@project, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- else
- = link_to 'Resume', resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
+ = link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm'
- if runner.belongs_to_one_project?
- = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ = link_to _('Remove Runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner)
- = link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- - elsif runner.specific?
+ = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
+ - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete
= form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
- = f.submit 'Enable for this project', class: 'btn btn-sm'
- .pull-right
+ = f.submit _('Enable for this project'), class: 'btn btn-sm'
+ .float-right
%small.light
\##{runner.id}
- if runner.description.present?
@@ -39,5 +39,5 @@
- if runner.tag_list.present?
%p
- runner.tag_list.sort.each do |tag|
- %span.label.label-primary
+ %span.badge.badge-primary
= tag
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 4fd4ca355a8..20a5ef039f8 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,4 +1,5 @@
-%h3 Shared Runners
+%h3
+ = _('Shared Runners')
.bs-callout.shared-runners-description
- if Gitlab::CurrentSettings.shared_runners_text.present?
@@ -17,8 +18,7 @@
&nbsp; for this project
- if @shared_runners_count.zero?
- This GitLab server does not provide any shared Runners yet.
- Please use the specific Runners or ask your administrator to create one.
+ = _('This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area.')
- else
%h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index f0813e56b71..6c11ce3b394 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,4 +1,5 @@
-%h3 Specific Runners
+%h3
+ = _('Specific Runners')
= render partial: 'ci/runner/how_to_setup_specific_runner',
locals: { registration_token: @project.runners_token }
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index 78dc4817ed7..d59f9c19862 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -1,6 +1,6 @@
-- page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners"
+- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", 'Runners'
%h4 Runner ##{@runner.id}
%hr
- = render 'form', runner: @runner, runner_form_url: runner_path(@runner)
+ = render 'shared/runners/form', runner: @runner, runner_form_url: project_runner_path(@project, @runner)
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
deleted file mode 100644
index f33e7e25b68..00000000000
--- a/app/views/projects/runners/show.html.haml
+++ /dev/null
@@ -1,67 +0,0 @@
-- page_title "#{@runner.description} ##{@runner.id}", "Runners"
-
-%h3.page-title
- Runner ##{@runner.id}
- .pull-right
- - if @runner.shared?
- %span.runner-state.runner-state-shared
- Shared
- - else
- %span.runner-state.runner-state-specific
- Specific
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Property Name
- %th Value
- %tr
- %td Active
- %td= @runner.active? ? 'Yes' : 'No'
- %tr
- %td Protected
- %td= @runner.ref_protected? ? 'Yes' : 'No'
- %tr
- %td Can run untagged jobs
- %td= @runner.run_untagged? ? 'Yes' : 'No'
- %tr
- %td Locked to this project
- %td= @runner.locked? ? 'Yes' : 'No'
- %tr
- %td Tags
- %td
- - @runner.tag_list.sort.each do |tag|
- %span.label.label-primary
- = tag
- %tr
- %td Name
- %td= @runner.name
- %tr
- %td Version
- %td= @runner.version
- %tr
- %td IP Address
- %td= @runner.ip_address
- %tr
- %td Revision
- %td= @runner.revision
- %tr
- %td Platform
- %td= @runner.platform
- %tr
- %td Architecture
- %td= @runner.architecture
- %tr
- %td Description
- %td= @runner.description
- %tr
- %td Maximum job timeout
- %td= @runner.maximum_timeout_human_readable
- %tr
- %td Last contact
- %td
- - if @runner.contacted_at
- #{time_ago_in_words(@runner.contacted_at)} ago
- - else
- Never
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 684b082efbb..aa30ebdc3b8 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -9,7 +9,7 @@
- if @service.respond_to?(:detailed_description)
%p= @service.detailed_description
.col-lg-9
- = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
+ = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
- if @service.editable?
.footer-block.row-content-block
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index 915c6b22162..acbab8b85c9 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -8,13 +8,13 @@
%colgroup
%col
%col
- %col.hidden-xs
+ %col.d-none.d-sm-block
%col{ width: "120" }
%thead
%tr
%th
%th Service
- %th.hidden-xs Description
+ %th.d-none.d-sm-block Description
%th Last edit
- @services.sort_by(&:title).each do |service|
%tr
@@ -23,9 +23,8 @@
%td
= link_to edit_project_service_path(@project, service.to_param) do
%strong= service.title
- %td.hidden-xs
+ %td.d-none.d-sm-block
= service.description
%td.light
- if service.updated_at.present?
- = time_ago_in_words service.updated_at
- ago
+ = time_ago_with_tooltip service.updated_at
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 2ab0227126a..209b9c71390 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -18,22 +18,22 @@
.help-form
.form-group
- = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#display_name')
+ = label_tag :display_name, 'Display name', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#display_name', class: 'input-group-text')
.form-group
- = label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#description')
+ = label_tag :description, 'Description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#description', class: 'input-group-text')
.form-group
- = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
+ = label_tag nil, 'Command trigger word', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
@@ -42,47 +42,47 @@
%code= @project.full_path
.form-group
- = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#request_url')
+ = label_tag :request_url, 'Request URL', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :request_url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#request_url', class: 'input-group-text')
.form-group
- = label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
+ = label_tag nil, 'Request method', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block POST
.form-group
- = label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#response_username')
+ = label_tag :response_username, 'Response username', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :response_username, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#response_username', class: 'input-group-text')
.form-group
- = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#response_icon')
+ = label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#response_icon', class: 'input-group-text')
.form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Yes
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block Yes
.form-group
- = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_hint')
+ = label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_hint', class: 'input-group-text')
.form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_description')
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
%hr
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index 2a1b9d4c465..62bef77be97 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,6 +1,6 @@
- enabled = Gitlab.config.mattermost.enabled
-.well
+.card
%p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost.
diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
index 44c0b7a90dc..2da8e5428ec 100644
--- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml
@@ -1,7 +1,7 @@
.services-installation-info
- unless @service.activated?
.row
- .col-sm-9.col-sm-offset-3
+ .col-sm-9.offset-sm-3
= link_to new_project_mattermost_path(@project), class: 'btn btn-lg' do
= custom_icon('mattermost_logo', size: 15)
Add to Mattermost
diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml
index 88acb824ba7..15e7362c2ba 100644
--- a/app/views/projects/services/prometheus/_help.html.haml
+++ b/app/views/projects/services/prometheus/_help.html.haml
@@ -5,5 +5,5 @@
= s_('PrometheusService|Manual configuration')
- unless @service.editable?
- .well
+ .card
= s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters')
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index 43e6a173108..bda597cc02b 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -7,12 +7,12 @@
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
.col-lg-9
- .panel.panel-default.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } }
- .panel-heading
- %h3.panel-title
+ .card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(@project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/metrics') } }
+ .card-header
+ %h3.card-title
= s_('PrometheusService|Common metrics')
- %span.badge.js-monitored-count 0
- .panel-body
+ %span.badge.badge-pill.js-monitored-count 0
+ .card-body
.loading-metrics.js-loading-metrics
%p.prepend-top-10.prepend-left-10
= icon('spinner spin', class: 'metrics-load-spinner')
@@ -22,13 +22,13 @@
= s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics')
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
- .panel.panel-default.hidden.js-panel-missing-env-vars
- .panel-heading
- %h3.panel-title
+ .card.hidden.js-panel-missing-env-vars
+ .card-header
+ %h3.card-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
= s_('PrometheusService|Missing environment variable')
- %span.badge.js-env-var-count 0
- .panel-body.hidden
+ %span.badge.badge-pill.js-env-var-count 0
+ .card-body.hidden
.flash-container
.flash-notice
.flash-text
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index d592a5e4663..44f58ad05e5 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,7 +1,7 @@
- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
-.well
+.card
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
@@ -26,8 +26,8 @@
.help-form
.form-group
- = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
+ = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
@@ -36,53 +36,53 @@
%code= @project.full_path
.form-group
- = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#url')
+ = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#url', class: 'input-group-text')
.form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
+ = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block POST
.form-group
- = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#customize_name')
+ = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#customize_name', class: 'input-group-text')
.form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
= link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block Show this command in the autocomplete list
.form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_description')
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
.form-group
- = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#autocomplete_usage_hint')
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
.form-group
- = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.input-group
- = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly'
- .input-group-btn
- = clipboard_button(target: '#descriptive_label')
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
%hr
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
new file mode 100644
index 00000000000..988bcfb5265
--- /dev/null
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -0,0 +1,43 @@
+.row.prepend-top-default
+ .col-lg-12
+ = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
+ = form_errors(@project)
+ %fieldset.builds-feature
+ .form-group
+ - message = auto_devops_warning_message(@project)
+ - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
+ - if message
+ %p.settings-message.text-center
+ = message.html_safe
+ = f.fields_for :auto_devops_attributes, @auto_devops do |form|
+ .form-check
+ = form.label :enabled_true do
+ = form.radio_button :enabled, 'true'
+ %strong= s_('CICD|Enable Auto DevOps')
+ %br
+ = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
+
+ .form-check
+ = form.label :enabled_false do
+ = form.radio_button :enabled, 'false'
+ %strong= s_('CICD|Disable Auto DevOps')
+ %br
+ = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
+
+ .form-check
+ = form.label :enabled_ do
+ = form.radio_button :enabled, ''
+ %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
+ %br
+ = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
+
+ = form.label :domain, class:"prepend-top-10" do
+ = _('Domain')
+ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
+ .help-block
+ = s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.')
+ - if cluster_ingress_ip = cluster_ingress_ip(@project)
+ = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
+
+ = f.submit 'Save changes', class: "btn btn-success prepend-top-15"
diff --git a/app/views/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml
index e8028059487..d14360913a4 100644
--- a/app/views/projects/settings/ci_cd/_badge.html.haml
+++ b/app/views/projects/settings/ci_cd/_badge.html.haml
@@ -2,15 +2,15 @@
.col-lg-12
%h4
= badge.title.capitalize
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%b
= badge.title.capitalize
&middot;
= badge.to_html
- .pull-right
+ .float-right
= render 'shared/ref_switcher', destination: 'badges', align_right: true
- .panel-body
+ .card-body
.row
.col-md-2.text-center
Markdown
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 20868f9ba5d..50175f5258c 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -3,50 +3,12 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
- .form-group
- %h5 Auto DevOps (Beta)
- %p
- Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
- = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- - message = auto_devops_warning_message(@project)
- - if message
- %p.settings-message.text-center
- = message.html_safe
- = f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .radio
- = form.label :enabled_true do
- = form.radio_button :enabled, 'true'
- %strong Enable Auto DevOps
- %br
- %span.descr
- The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
-
- .radio
- = form.label :enabled_false do
- = form.radio_button :enabled, 'false'
- %strong Disable Auto DevOps
- %br
- %span.descr
- An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
-
- .radio
- = form.label :enabled_ do
- = form.radio_button :enabled, ''
- %strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
- %br
- %span.descr
- Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
- %p
- You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
- = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
-
- %hr
.form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder
= '*' * 20
- = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used by the Runner to checkout the project
+ = f.text_field :runners_token, class: "form-control hidden js-secret-value", placeholder: 'xEeFCaDAB89'
+ %p.form-text.text-muted The secure token used by the Runner to checkout the project
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
@@ -57,14 +19,14 @@
%p
Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
- .radio
+ .form-check
= f.label :build_allow_git_fetch_false do
= f.radio_button :build_allow_git_fetch, 'false'
%strong git clone
%br
%span.descr
Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
- .radio
+ .form-check
= f.label :build_allow_git_fetch_true do
= f.radio_button :build_allow_git_fetch, 'true'
%strong git fetch
@@ -76,7 +38,7 @@
.form-group
= f.label :build_timeout_human_readable, 'Timeout', class: 'label-light'
= f.text_field :build_timeout_human_readable, class: 'form-control'
- %p.help-block
+ %p.form-text.text-muted
Per job. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
@@ -84,17 +46,17 @@
.form-group
= f.label :ci_config_path, 'Custom CI config path', class: 'label-light'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
- %p.help-block
+ %p.form-text.text-muted
The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr
.form-group
- .checkbox
+ .form-check
= f.label :public_builds do
= f.check_box :public_builds
%strong Public pipelines
- .help-block
+ .form-text.text-muted
Allow public access to pipelines and job details, including output logs and artifacts
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
.bs-callout.bs-callout-info
@@ -112,11 +74,11 @@
%hr
.form-group
- .checkbox
+ .form-check
= f.label :auto_cancel_pending_pipelines do
= f.check_box :auto_cancel_pending_pipelines, {}, 'enabled', 'disabled'
%strong Auto-cancel redundant, pending pipelines
- .help-block
+ .form-text.text-muted
New pipelines will cancel older, pending pipelines on the same branch
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank'
@@ -124,10 +86,12 @@
.form-group
= f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
.input-group
- %span.input-group-addon /
+ %span.input-group-prepend
+ .input-group-text /
= f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression'
- %span.input-group-addon /
- %p.help-block
+ %span.input-group-append
+ .input-group-text /
+ %p.form-text.text-muted
A regular expression that will be used to find the test coverage
output in the job trace. Leave blank to disable
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 09268c9943b..7d8dd58e7e0 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
- Update your CI/CD configuration, like job timeout or Auto DevOps.
+ Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
.settings-content
= render 'form'
+%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = s_('CICD|Auto DevOps')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
+ = link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
+ .settings-content
+ = render 'autodevops_form'
+
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index cd003107d66..77d88aed883 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -5,7 +5,7 @@
%div
- ProjectHook.triggers.each_value do |event|
- if hook.public_send(event)
- %span.label.label-gray.deploy-project-label= event.to_s.titleize
+ %span.badge.badge-gray.deploy-project-label= event.to_s.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index f57590a908f..5dda2ec28b4 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -2,6 +2,8 @@
- page_title "Repository"
- @content_class = "limit-container-width" unless fluid_layout
+= render "projects/mirrors/show"
+
-# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory.
-# Those are used throughout the actual views. These `shared` views are then
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index f09871c7fcc..b3a9fa9dd91 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,6 +1,6 @@
- return unless current_user
-.hidden-xs
+.d-none.d-sm-block
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_project_snippet_path(@project, @snippet), class: "btn btn-grouped" do
Edit
@@ -13,7 +13,7 @@
- if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
- .visible-xs-block.dropdown
+ .d-block.d-sm-none.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
= icon('caret-down')
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 98b4d6339da..f55202c2c5f 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -6,7 +6,7 @@
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4'
- if protected_tag?(@project, tag)
- %span.label.label-success.prepend-left-4
+ %span.badge.badge-success.prepend-left-4
= s_('TagsPage|protected')
- if tag.message.present?
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 10415d011d6..96ecac815c0 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -16,7 +16,7 @@
%span.light
= tags_sort_options_hash[@sort]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= s_('TagsPage|Sort by')
- tags_sort_options_hash.each do |value, title|
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 1827a3d323c..b596748ca5f 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -10,35 +10,35 @@
= s_('TagsPage|New Tag')
%hr
-= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal common-note-form tag-form js-quick-submit js-requires-input" do
- .form-group
- = label_tag :tag_name, nil, class: 'control-label'
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "common-note-form tag-form js-quick-submit js-requires-input" do
+ .form-group.row
+ = label_tag :tag_name, nil, class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag :tag_name, params[:tag_name], required: true, autofocus: true, class: 'form-control'
- .form-group
- = label_tag :ref, 'Create from', class: 'control-label'
+ .form-group.row
+ = label_tag :ref, 'Create from', class: 'col-form-label col-sm-2'
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, default_ref
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref
= render 'shared/ref_dropdown', dropdown_class: 'wide'
- .help-block
+ .form-text.text-muted
= s_('TagsPage|Existing branch name, tag, or commit SHA')
- .form-group
- = label_tag :message, nil, class: 'control-label'
+ .form-group.row
+ = label_tag :message, nil, class: 'col-form-label col-sm-2'
.col-sm-10
= text_area_tag :message, @message, required: false, class: 'form-control', rows: 5
- .help-block
+ .form-text.text-muted
= s_('TagsPage|Optionally, add a message to the tag.')
%hr
- .form-group
- = label_tag :release_description, s_('TagsPage|Release notes'), class: 'control-label'
+ .form-group.row
+ = label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description
= render 'shared/notes/hints'
- .help-block
+ .form-text.text-muted
= s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.')
.form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 7a3469cdd26..15a960f81b8 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -11,7 +11,7 @@
= icon('tag')
= @tag.name
- if protected_tag?(@project, @tag)
- %span.label.label-success
+ %span.badge.badge-success
= s_('TagsPage|protected')
- if @commit
= render 'projects/branches/commit', commit: @commit, project: @project
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index 8c1c532cb3e..f79f3af36d4 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -6,7 +6,7 @@
= 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.label.label-lfs.prepend-left-5 LFS
- %td.hidden-xs.tree-commit
+ %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/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml
index 04d52361db0..e563c8c4036 100644
--- a/app/views/projects/tree/_submodule_item.html.haml
+++ b/app/views/projects/tree/_submodule_item.html.haml
@@ -3,4 +3,4 @@
%i.fa.fa-archive.fa-fw
= submodule_link(submodule_item, @ref)
%td
- %td.hidden-xs
+ %td.d-none.d-sm-table-cell
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 6ea78851b8d..587aeafa82f 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -4,15 +4,15 @@
%thead
%tr
%th= s_('ProjectFileTree|Name')
- %th.hidden-xs
- .pull-left= _('Last commit')
+ %th.d-none.d-sm-table-cell
+ .float-left= _('Last commit')
%th.text-right= _('Last update')
- if @path.present?
%tr.tree-item
%td.tree-item-file-name
= link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
%td
- %td.hidden-xs
+ %td.d-none.d-sm-table-cell
= render_tree(tree)
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 8587d3b0c0d..4fc1a284693 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -11,18 +11,18 @@
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
%ul.breadcrumb.repo-breadcrumb
- %li
+ %li.breadcrumb-item
= link_to project_tree_path(@project, @ref) do
= @project.path
- path_breadcrumbs do |title, path|
- %li
+ %li.breadcrumb-item
= link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
- if can_collaborate || can_create_mr_from_fork
- %li
+ %li.breadcrumb-item
%a.btn.add-to-tree{ addtotree_toggle_attributes }
- = sprite_icon('plus', size: 16, css_class: 'pull-left')
- = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
+ = sprite_icon('plus', size: 16, css_class: 'float-left')
+ = sprite_icon('arrow-down', size: 16, css_class: 'float-left')
- if on_top_of_branch?
.add-to-tree-dropdown
%ul.dropdown-menu
@@ -82,7 +82,7 @@
- if can_collaborate
= succeed " " do
- = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do
+ = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default' do
= _('Web IDE')
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index af3816fc9f4..ce0cd95b468 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -4,6 +4,6 @@
- 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.hidden-xs.tree-commit
+ %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/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml
index 6c2d603d95d..96a41aa066c 100644
--- a/app/views/projects/triggers/_content.html.haml
+++ b/app/views/projects/triggers/_content.html.haml
@@ -1,6 +1,6 @@
%p.append-bottom-default
Triggers with the
- %span.label.label-primary legacy
+ %span.badge.badge-primary legacy
label do not have an associated user and only have access to the current project.
%br
= succeed '.' do
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index 5f708b3a2ed..3539aea3580 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -4,7 +4,7 @@
- if @trigger.token
.form-group
%label.label-light Token
- %p.form-control-static= @trigger.token
+ %p.form-control-plaintext= @trigger.token
.form-group
= f.label :key, "Description", class: "label-light"
= f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index 0f655e4ed83..a15bb4c4f3f 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,11 +1,11 @@
.row.prepend-top-default.append-bottom-default.triggers-container
.col-lg-12
= render "projects/triggers/content"
- .panel.panel-default
- .panel-heading
- %h4.panel-title
+ .card
+ .card-header
+ %h4.card-title
Manage your project's triggers
- .panel-body
+ .card-body
= render "projects/triggers/form", btn_text: "Add trigger"
%hr
- if @triggers.any?
@@ -26,7 +26,7 @@
%p.settings-message.text-center.append-bottom-default
No triggers have been created yet. Add one using the form above.
- .panel-footer
+ .card-footer
%p
In the following examples, you can see the exact API call you need to
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 6249c32b7cc..7e4618e1a88 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -8,9 +8,9 @@
.label-container
- if trigger.legacy?
- %span.label.label-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
+ %span.badge.badge-primary.has-tooltip{ title: "Trigger makes use of deprecated functionality" } legacy
- if !trigger.can_access_project?
- %span.label.label-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
+ %span.badge.badge-danger.has-tooltip{ title: "Trigger user has insufficient permissions to project" } invalid
%td
- if trigger.description? && trigger.description.length > 15
@@ -25,7 +25,7 @@
%td
- if trigger.last_used
- #{time_ago_in_words(trigger.last_used)} ago
+ = time_ago_with_tooltip trigger.last_used
- else
Never
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index d285251d06f..bcceb69954a 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,13 +1,13 @@
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title }
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
- if @page.persisted?
= f.hidden_field :last_commit_sha, value: @page.last_commit_sha
- .form-group
+ .form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
= f.text_field :title, class: 'form-control', value: @page.title
@@ -16,12 +16,12 @@
= icon('lightbulb-o')
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
- .form-group
+ .form-group.row
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
= f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control'
- .form-group
+ .form-group.row
.col-sm-12= f.label :content, class: 'control-label-full-width'
.col-sm-12
= render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do
@@ -31,7 +31,7 @@
.clearfix
.error-alert
- .help-block
+ .form-text.text-muted
= succeed '.' do
= (s_("WikiMarkdownTip|To link to a (new) page, simply type %{link_example}") % { link_example: '<code>[Link Title](page-slug)</code>' }).html_safe
@@ -39,16 +39,16 @@
- markdown_link = link_to s_("WikiMarkdownDocs|documentation"), help_page_path('user/markdown', anchor: 'wiki-specific-markdown')
= (s_("WikiMarkdownDocs|More examples are in the %{docs_link}") % { docs_link: markdown_link }).html_safe
- .form-group
+ .form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
.col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
= f.submit _("Save changes"), class: 'btn-save btn'
- .pull-right
+ .float-right
= link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped'
- else
= f.submit s_("Wiki|Create page"), class: 'btn-create btn'
- .pull-right
+ .float-right
= link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel'
diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml
index efa16d38f84..cbb441d7509 100644
--- a/app/views/projects/wikis/_pages_wiki_page.html.haml
+++ b/app/views/projects/wikis/_pages_wiki_page.html.haml
@@ -1,5 +1,5 @@
%li
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
- .pull-right
+ .float-right
%small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 2c7551c6f8c..a23396dc0d8 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,7 +1,7 @@
%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } }
.sidebar-container
.block.wiki-sidebar-header.append-bottom-default
- %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
+ %a.gutter-toggle.float-right.d-block.d-sm-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
- git_access_url = project_wikis_git_access_path(@project)
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 9d3d4072027..35c7dc2984a 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -28,9 +28,16 @@
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project)
- = link_to project_wiki_path(@project, @page), data: { confirm: s_("WikiPageConfirmDelete|Are you sure you want to delete this page?")}, method: :delete, class: "btn btn-danger" do
- = _("Delete")
+ %button.btn.btn-danger{ data: { toggle: 'modal',
+ target: '#delete-wiki-modal',
+ delete_wiki_url: project_wiki_path(@project, @page),
+ page_title: @page.title.capitalize },
+ id: 'delete-wiki-button',
+ type: 'button' }
+ = _('Delete')
= render 'form'
= render 'sidebar'
+
+#delete-wiki-modal.modal.fade
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 10dbbc0e42c..909987d8090 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -2,7 +2,7 @@
- page_title s_("WikiClone|Git Access"), _("Wiki")
.wiki-page-header.has-sidebar-toggle
- %button.btn.btn-default.visible-xs.visible-sm.pull-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
+ %button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
.git-access-header
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index b3b83cee81a..ff72c8bb75d 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -24,7 +24,7 @@
- history_link = link_to s_("WikiHistoricalPage|history"), project_wiki_history_path(@project, @page)
= (s_("WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}.") % { most_recent_link: most_recent_link, history_link: history_link }).html_safe
-.wiki-holder.prepend-top-default.append-bottom-default
+.prepend-top-default.append-bottom-default
.wiki
= render_wiki_content(@page)
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 7d43fd61081..ff9a7b53a86 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,80 +1,80 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.search-filter.scrolling-tabs
+ %ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs
- if @project
- if project_search_tabs?(:blobs)
%li{ class: active_when(@scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do
Code
- %span.badge
+ %span.badge.badge-pill
= @search_results.blobs_count
- if project_search_tabs?(:issues)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
- if project_search_tabs?(:merge_requests)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
- if project_search_tabs?(:milestones)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
- if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
Comments
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_notes_count)
- if project_search_tabs?(:wiki)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
Wiki
- %span.badge
+ %span.badge.badge-pill
= @search_results.wiki_blobs_count
- if project_search_tabs?(:commits)
%li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do
Commits
- %span.badge
+ %span.badge.badge-pill
= @search_results.commits_count
- elsif @show_snippets
%li{ class: active_when(@scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
Snippet Contents
- %span.badge
+ %span.badge.badge-pill
= @search_results.snippet_blobs_count
%li{ class: active_when(@scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
Titles and Filenames
- %span.badge
+ %span.badge.badge-pill
= @search_results.snippet_titles_count
- else
%li{ class: active_when(@scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do
Projects
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_projects_count)
%li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do
Issues
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_issues_count)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_merge_requests_count)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
- %span.badge
+ %span.badge.badge-pill
= limited_count(@search_results.limited_milestones_count)
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index e4902d368e7..6837f64f132 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -11,7 +11,7 @@
- else
Any
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title("Filter results by group")
= dropdown_filter("Search groups")
= dropdown_content
@@ -26,7 +26,7 @@
- else
Any
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title("Filter results by project")
= dropdown_filter("Search projects")
= dropdown_content
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index b7a27ef6be2..c413c3d4abb 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -4,8 +4,8 @@
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
- if issue.closed?
- %span.label.label-danger.prepend-left-5 Closed
- .pull-right ##{issue.iid}
+ %span.badge.badge-danger.prepend-left-5 Closed
+ .float-right ##{issue.iid}
- if issue.description.present?
.description.term
= search_md_sanitize(issue, :description)
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 8b0fd74f680..519176af108 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -3,10 +3,10 @@
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
- if merge_request.merged?
- %span.label.label-primary.prepend-left-5 Merged
+ %span.badge.badge-primary.prepend-left-5 Merged
- elsif merge_request.closed?
- %span.label.label-danger.prepend-left-5 Closed
- .pull-right= merge_request.to_reference
+ %span.badge.badge-danger.prepend-left-5 Closed
+ .float-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
= search_md_sanitize(merge_request, :description)
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index d46c4d11e51..9c8afb2165b 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -3,13 +3,13 @@
= link_to reliable_snippet_path(snippet_title) do
= truncate(snippet_title.title, length: 60)
- if snippet_title.private?
- %span.label.label-gray
+ %span.badge.badge-gray
%i.fa.fa-lock
private
- %span.cgray.monospace.tiny.pull-right.term
+ %span.cgray.monospace.tiny.float-right.term
= snippet_title.file_name
- %small.pull-right.cgray
+ %small.float-right.cgray
- if snippet_title.project_id?
= link_to snippet_title.project.full_name, project_path(snippet_title.project)
diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml
index 53a99a736c0..0e698570d7d 100644
--- a/app/views/shared/_allow_request_access.html.haml
+++ b/app/views/shared/_allow_request_access.html.haml
@@ -1,4 +1,4 @@
-.checkbox
+.form-check
= form.label :request_access_enabled do
= form.check_box :request_access_enabled
%strong Allow users to request access
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index e9ac192f5f7..084d295f2c1 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -3,13 +3,13 @@
= custom_icon('icon_autodevops')
.banner-body.prepend-left-10.append-bottom-10
- %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)')
+ %h5.banner-title= s_('AutoDevOps|Auto DevOps')
%p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
%p
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
.banner-buttons
- = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
+ = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' }
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index 75c65520350..0552fe62090 100644
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ b/app/views/shared/_choose_group_avatar_button.html.haml
@@ -1,4 +1,4 @@
%button.btn.js-choose-group-avatar-button{ type: 'button' }= _("Choose File ...")
%span.file_name.js-avatar-filename= _("No file chosen")
= f.file_field :avatar, class: "js-group-avatar-input hidden"
-.help-block= _("The maximum file size allowed is 200KB.")
+.form-text.text-muted= _("The maximum file size allowed is 200KB.")
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 687cd4d1532..5fc02ba3160 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,13 +1,13 @@
- project = project || @project
.git-clone-holder.input-group
- .input-group-btn
+ .input-group-prepend
- if allowed_protocols_present?
- .clone-dropdown-btn.btn
+ .input-group-text.clone-dropdown-btn.btn
%span
= enabled_project_button(project, enabled_protocol)
- else
- %a#clone-dropdown.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
%span
= default_clone_protocol.upcase
= icon('caret-down')
@@ -18,5 +18,5 @@
= http_clone_button(project)
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
- .input-group-btn
- = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "btn-default btn-clipboard")
+ .input-group-append
+ = clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 2329de9e11f..68c14c307ac 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,7 +1,7 @@
-.form-group.commit_message-group
+.form-group.row.commit_message-group
- nonce = SecureRandom.hex
- descriptions = local_assigns.slice(:message_with_description, :message_without_description)
- = label_tag "commit_message-#{nonce}", class: 'control-label' do
+ = label_tag "commit_message-#{nonce}", class: 'col-form-label col-sm-2' do
#{ _('Commit message') }
.col-sm-10
.commit-message-container
diff --git a/app/views/shared/_commit_well.html.haml b/app/views/shared/_commit_well.html.haml
index 50e3d80a84d..6f1fe9bfdc5 100644
--- a/app/views/shared/_commit_well.html.haml
+++ b/app/views/shared/_commit_well.html.haml
@@ -1,4 +1,4 @@
-.info-well.hidden-xs.project-last-commit.append-bottom-default
+.info-well.d-none.d-sm-block.project-last-commit.append-bottom-default
.well-segment
%ul.blob-commit-info
= render 'projects/commits/commit', commit: commit, ref: ref, project: project
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index f94f8ffc604..7c326d36d99 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -2,9 +2,9 @@
.modal-dialog
.modal-content
.modal-header
- %a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title
Confirmation required
+ %a.close{ href: "#", "data-dismiss" => "modal" } ×
.modal-body
%p.text-danger.js-confirm-text
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index e7fa7477e0c..ecb5b1c6ebc 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,7 +1,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.event-filter.scrolling-tabs
+ %ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index aea0a8fd8e0..0c8d90d92f5 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -11,9 +11,9 @@
.form-group
- if type == "password" && value.present?
- = form.label name, "Enter new #{title.downcase}", class: "control-label"
+ = form.label name, "Enter new #{title.downcase}", class: "col-form-label"
- else
- = form.label name, title, class: "control-label"
+ = form.label name, title, class: "col-form-label"
.col-sm-10
- if type == 'text'
= form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
@@ -26,4 +26,4 @@
- elsif type == 'password'
= form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required, disabled: disabled
- if help
- %span.help-block= help
+ %span.form-text.text-muted= help
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index cb21f90696f..dbed4b94d61 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -2,15 +2,16 @@
- group_path = root_url
- group_path << parent.full_path + '/' if parent
-.form-group
- = f.label :path, class: 'control-label' do
+.form-group.row
+ = f.label :path, class: 'col-form-label col-sm-2' do
Group path
.col-sm-10
.input-group.gl-field-error-anchor
- .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
- %span>= root_url
- - if parent
- %strong= parent.full_path + '/'
+ .group-root-path.input-group-prepend.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
+ .input-group-text
+ %span>= root_url
+ - if parent
+ %strong= parent.full_path + '/'
= f.hidden_field :parent_id
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
@@ -24,16 +25,23 @@
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
-.form-group.group-name-holder
- = f.label :name, class: 'control-label' do
+.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.'
-.form-group.group-description-holder
- = f.label :description, class: 'control-label'
+- 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
+ = 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
= f.text_area :description, maxlength: 250,
class: 'form-control js-gfm-input', rows: 4
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 3806ead6c87..35673303b85 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,13 +1,13 @@
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
-.form-group.import-url-data
+.form-group.row.import-url-data
= f.label :import_url, class: 'label-light' do
%span
= _('Git repository URL')
= f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
- .well.prepend-top-20
+ .card.prepend-top-20
%ul
%li
= _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 430d9a9dd76..6cc8c485666 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -5,21 +5,21 @@
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0
- %li.issuable-mr.hidden-xs.has-tooltip{ title: _('Related merge requests') }
+ %li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') }
= image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
= issuable_mr
- if upvotes > 0
- %li.issuable-upvotes.hidden-xs.has-tooltip{ title: _('Upvotes') }
+ %li.issuable-upvotes.d-none.d-sm-block.has-tooltip{ title: _('Upvotes') }
= icon('thumbs-up')
= upvotes
- if downvotes > 0
- %li.issuable-downvotes.hidden-xs.has-tooltip{ title: _('Downvotes') }
+ %li.issuable-downvotes.d-none.d-sm-block.has-tooltip{ title: _('Downvotes') }
= icon('thumbs-down')
= downvotes
-%li.issuable-comments.hidden-xs
+%li.issuable-comments.d-none.d-sm-block
= link_to issuable_url, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do
= icon('comments')
= note_count
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 49555b6ff4e..987a5d4f13f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,5 +1,5 @@
- if @issues.to_a.any?
- .panel.panel-default.panel-small.panel-without-border
+ .card.card-small.card-without-border
%ul.content-list.issues-list.issuable-list
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index 0517896cfbd..700ec4b606f 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,5 +1,5 @@
- if @merge_requests.to_a.any?
- .panel.panel-default.panel-small.panel-without-border
+ .card.card-small.card-without-border
%ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 034b76b978f..6c1ac20d544 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,13 +1,13 @@
-%ul.nav-links.mobile-separator
+%ul.nav-links.mobile-separator.nav.nav-tabs
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
Open
- %span.badge= counts[:opened]
+ %span.badge.badge-pill= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do
Closed
- %span.badge= counts[:closed]
+ %span.badge.badge-pill= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do
All
- %span.badge= counts[:all]
+ %span.badge.badge-pill= counts[:all]
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index 9b2f2fdcc93..a6ba3b59365 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -6,7 +6,7 @@
- else
= sort_title_due_date_soon
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li
= link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
= sort_title_due_date_soon
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index 901a177323b..ac2164a4a71 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -6,12 +6,13 @@
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.dropdown{ class: klass }
- %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
= sprite_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
%li.js-builds-dropdown-list.scrollable-menu
+ %ul
%li.js-builds-dropdown-loading.hidden
.text-center
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
index 9221fd1e025..81c33eeea4f 100644
--- a/app/views/shared/_new_commit_form.html.haml
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -7,8 +7,8 @@
= hidden_field_tag 'branch_name', @ref
- else
- if can?(current_user, :push_code, @project)
- .form-group.branch
- = label_tag 'branch_name', _('Target Branch'), class: 'control-label'
+ .form-group.row.branch
+ = label_tag 'branch_name', _('Target Branch'), class: 'col-form-label col-sm-2'
.col-sm-10
= text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name"
diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml
index 133c31f09c4..8dbdf63f9f4 100644
--- a/app/views/shared/_new_merge_request_checkbox.html.haml
+++ b/app/views/shared/_new_merge_request_checkbox.html.haml
@@ -1,4 +1,4 @@
-.checkbox
+.form-check
- nonce = SecureRandom.hex
= label_tag "create_merge_request-#{nonce}" do
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
index c5e4d6e2871..cadac1cc99d 100644
--- a/app/views/shared/_personal_access_tokens_table.html.haml
+++ b/app/views/shared/_personal_access_tokens_table.html.haml
@@ -35,7 +35,7 @@
= 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 pull-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
+ %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
.settings-message.text-center
This user has no active #{type} Tokens.
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index f4eb8e491b9..2c52eccccb6 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
- .project-limit-message.alert.alert-warning.hidden-xs
+ .project-limit-message.alert.alert-warning.d-none.d-sm-block
You won't be able to create new projects because you have reached your project limit.
- .pull-right
+ .float-right
= link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link'
|
= link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link'
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index f1c39b9e923..4e7061eef1c 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -9,7 +9,7 @@
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
- .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
new file mode 100644
index 00000000000..34de1c0695f
--- /dev/null
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -0,0 +1,13 @@
+- if @project.has_remote_mirror?
+ .append-bottom-default
+ - if remote_mirror.update_in_progress?
+ %span.btn.disabled
+ = icon("refresh spin")
+ Updating&hellip;
+ - else
+ = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do
+ = icon("refresh")
+ Update Now
+ - if @remote_mirror.last_successful_update_at
+ %p.inline.prepend-left-10
+ Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index a41aaed66a3..0ebf365c7bd 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -3,24 +3,24 @@
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: subject
- elsif @service.help.present?
- .well
+ .card
= markdown @service.help
.service-settings
- if @service.show_active_box?
- .form-group
- = form.label :active, "Active", class: "control-label"
+ .form-group.row
+ = form.label :active, "Active", class: "col-form-label col-sm-2"
.col-sm-10
= form.check_box :active, disabled: disable_fields_service?(@service)
- if @service.configurable_events.present?
- .form-group
- = form.label :url, "Trigger", class: 'control-label'
+ .form-group.row
+ = form.label :url, "Trigger", class: 'col-form-label col-sm-2'
.col-sm-10
- @service.configurable_events.each do |event|
%div
- = form.check_box service_event_field_name(event), class: 'pull-left'
+ = form.check_box service_event_field_name(event), class: 'float-left'
.prepend-left-20
= form.label service_event_field_name(event), class: 'list-label' do
%strong
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 7ff5e679f17..be6d4f1c32b 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -2,10 +2,10 @@
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
= sorted_by
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-sort
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority, label: true), sorted_by)
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by)
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 192d2502aaf..d67409ffe14 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -2,7 +2,7 @@
.form-group.visibility-level-setting
- if with_label
- = f.label :visibility_level, class: 'control-label' do
+ = f.label :visibility_level, class: 'col-form-label col-sm-2' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
%div{ :class => ("col-sm-10" if with_label) }
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index 0ec7677a566..bdf550cd8b6 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -2,7 +2,7 @@
- disallowed = disallowed_visibility_level?(form_model, level)
- restricted = restricted_visibility_levels.include?(level)
- disabled = disallowed || restricted
- .radio{ class: [('disabled' if disabled), ('restricted' if restricted)] }
+ .form-check.pl-0{ class: [('disabled' if disabled), ('restricted' if restricted)] }
= form.label "#{model_method}_#{level}" do
= form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled
= visibility_level_icon(level)
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index dac60094686..496b94ec953 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -13,7 +13,7 @@
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
- .hidden-xs.hidden-sm
+ .d-none.d-sm-none.d-md-block
= render 'shared/issuable/search_bar', type: :boards
.boards-list
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 149bf8da4b9..67476a3f573 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -14,17 +14,18 @@
%span.has-tooltip{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
- class: "label color-label title board-title-text",
- ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
+ class: "badge color-label title board-title-text",
+ ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.textColor ? list.label.textColor : \"#2e2e2e\") }" }
{{ list.title }}
+
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ %button.board-delete.has-tooltip.float-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
.issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
- %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
+ %span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent)
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index b385cc3f962..774dafe5f2c 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -3,14 +3,14 @@
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
.block.issuable-sidebar-header
- %span.issuable-header-text.hide-collapsed.pull-left
+ %span.issuable-header-text.hide-collapsed.float-left
%strong
{{ issue.title }}
%br/
%span
= precede "#" do
{{ issue.iid }}
- %a.gutter-toggle.pull-right{ role: "button",
+ %a.gutter-toggle.float-right{ role: "button",
href: "#",
"@click.prevent" => "closeSidebar",
"aria-label" => "Toggle sidebar" }
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
index d13b998e6f4..10217b6cbf0 100644
--- a/app/views/shared/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -3,7 +3,7 @@
Due date
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 87e6b52f46e..a112a9f1f7e 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -3,13 +3,13 @@
Labels
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
- .value.issuable-show-labels
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ .value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
None
%a{ href: "#",
"v-for" => "label in issue.labels" }
- %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ %span.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
- if can_admin_issue?
.selectbox
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
index f51c4a97f2b..f2bedd5e3c9 100644
--- a/app/views/shared/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -3,7 +3,7 @@
Milestone
- if can_admin_issue?
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
None
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 0b003125912..5c74e71b644 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,24 +1,24 @@
-%ul.nav-links.mobile-separator
+%ul.nav-links.mobile-separator.nav.nav-tabs
%li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
- %span.badge.js-totalbuilds-count
+ %span.badge.badge-pill.js-totalbuilds-count
= limited_counter_with_delimiter(all_builds)
%li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do
Pending
- %span.badge
+ %span.badge.badge-pill
= limited_counter_with_delimiter(all_builds.pending)
%li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do
Running
- %span.badge
+ %span.badge.badge-pill
= limited_counter_with_delimiter(all_builds.running)
%li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do
Finished
- %span.badge
+ %span.badge.badge-pill
= limited_counter_with_delimiter(all_builds.finished)
diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml
index 87c2965bb21..913c065e188 100644
--- a/app/views/shared/deploy_keys/_form.html.haml
+++ b/app/views/shared/deploy_keys/_form.html.haml
@@ -5,26 +5,26 @@
= form_errors(deploy_key)
.form-group
- = form.label :title, class: 'control-label'
+ = form.label :title, class: 'col-form-label col-sm-2'
.col-sm-10= form.text_field :title, class: 'form-control'
.form-group
- if deploy_key.new_record?
- = form.label :key, class: 'control-label'
+ = form.label :key, class: 'col-form-label col-sm-2'
.col-sm-10
%p.light
Paste a machine public key here. Read more about how to generate it
= link_to 'here', help_page_path('ssh/README')
= form.text_area :key, class: 'form-control thin_area', rows: 5
- else
- = form.label :fingerprint, class: 'control-label'
+ = form.label :fingerprint, class: 'col-form-label col-sm-2'
.col-sm-10
= form.text_field :fingerprint, class: 'form-control', readonly: 'readonly'
- if deploy_keys_project.present?
= form.fields_for :deploy_keys_projects, deploy_keys_project do |deploy_keys_project_form|
.form-group
- .control-label
+ .col-form-label.col-sm-2
.col-sm-10
= deploy_keys_project_form.label :can_push do
= deploy_keys_project_form.check_box :can_push
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 62437f5fc9d..c7c33288e9d 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/issues.svg'
- .col-xs-12
+ .col-12
.text-content
- if current_user
%h4
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 04db9de3606..e8749ee3956 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,8 +1,8 @@
.row.empty-state.labels
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/labels.svg'
- .col-xs-12
+ .col-12
.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.")
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 2edf3557df4..014220761a9 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -3,10 +3,10 @@
- has_button = button_path || project_select_button
.row.empty-state.merge-requests
- .col-xs-12
+ .col-12
.svg-content
= image_tag 'illustrations/merge_requests.svg'
- .col-xs-12
+ .col-12
.text-content
- if has_button
%h4
diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml
index 38e9899ca4b..e5dfa7dbf71 100644
--- a/app/views/shared/form_elements/_description.html.haml
+++ b/app/views/shared/form_elements/_description.html.haml
@@ -9,8 +9,8 @@
- else
- preview_url = preview_markdown_path(project)
-.form-group.detail-page-description
- = form.label :description, 'Description', class: 'control-label'
+.form-group.row.detail-page-description
+ = form.label :description, 'Description', class: 'col-form-label col-sm-2'
.col-sm-10
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 8607be9cd06..2237b93a10b 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -13,7 +13,7 @@
%span.dropdown-label
= options_hash[default_sort_by]
= icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- options_hash.each do |value, title|
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 90395600d4e..a1b901aaffa 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -16,7 +16,7 @@
.avatar-container.s40
= link_to group do
- = group_icon(group, class: "avatar s40 hidden-xs")
+ = group_icon(group, class: "avatar s40 d-none d-sm-block")
.title
= link_to group.full_name, group, class: 'group-name'
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index c80b179d525..532712ee6d1 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -6,8 +6,8 @@
%p
%strong Trigger:
- %td.hidden-xs
- %span.label.label-gray.deploy-project-label
+ %td.d-none.d-sm-block
+ %span.badge.badge-gray.deploy-project-label
= hook_log.trigger.singularize.titleize
%p
%strong Elapsed time:
diff --git a/app/views/shared/hook_logs/_status_label.html.haml b/app/views/shared/hook_logs/_status_label.html.haml
index b4ea8e6f952..993880b7d6e 100644
--- a/app/views/shared/hook_logs/_status_label.html.haml
+++ b/app/views/shared/hook_logs/_status_label.html.haml
@@ -1,3 +1,3 @@
-- label_status = hook_log.success? ? 'label-success' : 'label-danger'
+- label_status = hook_log.success? ? 'badge-success' : 'badge-danger'
%span{ class: "label #{label_status}" }
= hook_log.response_status
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 0d507cc7a6e..ca02424215c 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -4,9 +4,9 @@
.issuable-sidebar.hidden
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
.block.issuable-sidebar-header
- .filter-item.inline.update-issues-btn.pull-left
+ .filter-item.inline.update-issues-btn.float-left
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
- = button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right"
+ = button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide float-right"
.block
.title
Status
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 9ef015047c9..933d4b2ea65 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -4,11 +4,11 @@
- if can_update && is_current_user
= link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
- class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
= link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
- class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- else
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
- class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
+ class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
index 39a5171c1d6..0d59c9304b4 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -1,12 +1,12 @@
- display_issuable_type = issuable_display_type(issuable)
- button_action = issuable.closed? ? 'reopen' : 'close'
- display_button_action = button_action.capitalize
-- button_responsive_class = 'hidden-xs hidden-sm'
+- button_responsive_class = 'd-none d-sm-none d-md-block'
- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button"
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
- button_method = issuable_close_reopen_button_method(issuable)
-.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
+.float-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable),
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
@@ -14,7 +14,7 @@
data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
= icon('caret-down', class: 'toggle-icon icon')
- %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
+ %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ data: { dropdown: true } }
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
data: { text: "Close #{display_issuable_type}", url: close_issuable_path(issuable),
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 1bd5b4164b1..1cd8ce0826c 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -25,7 +25,7 @@
= 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
- .pull-right
+ .float-right
= render 'shared/sort_dropdown'
- has_labels = @labels && @labels.any?
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 4c8f03f1498..fbc608b207a 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -11,8 +11,8 @@
= link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs
-.form-group
- = form.label :title, class: 'control-label'
+.form-group.row
+ = form.label :title, class: 'col-form-label col-sm-2'
= render 'shared/issuable/form/template_selector', issuable: issuable
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
@@ -20,9 +20,9 @@
= render 'shared/form_elements/description', model: issuable, form: form, project: project
- if issuable.respond_to?(:confidential)
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
+ .form-group.row
+ .offset-sm-2.col-sm-10
+ .form-check
= form.label :confidential do
= form.check_box :confidential
This issue is confidential and should only be visible to team members with at least Reporter access.
@@ -36,8 +36,8 @@
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
- if @merge_request_to_resolve_discussions_of
- .form-group
- .col-sm-10.col-sm-offset-2
+ .form-group.row
+ .col-sm-10.offset-sm-2
= icon('info-circle')
- if @merge_request_to_resolve_discussions_of.discussions_can_be_resolved_by?(current_user)
= hidden_field_tag 'merge_request_to_resolve_discussions_of', @merge_request_to_resolve_discussions_of.iid
@@ -57,7 +57,7 @@
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
- .pull-right
+ .float-right
- if issuable.new_record?
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 91aa329eb93..9f4021802df 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -12,7 +12,7 @@
.dropdown-label-color-preview.js-dropdown-label-color-preview
%input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') }
.clearfix
- %button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" }
+ %button.btn.btn-primary.float-left.js-new-label-btn{ type: "button" }
= _('Create')
- %button.btn.btn-default.pull-right.js-cancel-label-btn{ type: "button" }
+ %button.btn.btn-default.float-right.js-cancel-label-btn{ type: "button" }
= _('Cancel')
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index a5f40ea934b..157637dbd11 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -2,7 +2,7 @@
- page_context_word = type.to_s.humanize(capitalize: false)
- display_count = local_assigns.fetch(:display_count, :true)
-%ul.nav-links.issues-state-filters.mobile-separator
+%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened, display_count)}
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index fc6f71ef60f..644f7c4dd28 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -107,7 +107,7 @@
.dropdown.prepend-left-10#js-add-list
%button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- if can?(current_user, :admin_label, board.parent)
= render partial: "shared/issuable/label_page_create"
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 975b9cb4729..602729b172a 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -5,9 +5,9 @@
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
- if current_user
- %span.issuable-header-text.hide-collapsed.pull-left
+ %span.issuable-header-text.hide-collapsed.float-left
= _('Todo')
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
@@ -19,22 +19,21 @@
.block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
.block.milestone
- .sidebar-collapsed-icon
+ .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
- if issuable.milestone
- %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
- = issuable.milestone.title
+ = issuable.milestone.title
- else
= _('None')
.title.hide-collapsed
= _('Milestone')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
+ = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 'true' }
- else
%span.no-value
= _('None')
@@ -50,7 +49,7 @@
= icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date)
.block.due_date
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable) }
= icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None'
@@ -58,7 +57,7 @@
= _('Due date')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
%span.value-content
- if issuable.due_date
@@ -87,7 +86,7 @@
- if @labels
- selected_labels = issuable.labels
.block.labels
- .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body", boundary: 'viewport' } }
= icon('tags', 'aria-hidden': 'true')
%span
= selected_labels.size
@@ -95,8 +94,8 @@
= _('Labels')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
- .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
= link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
@@ -134,7 +133,7 @@
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
.sidebar-collapsed-icon.dont-change-state
- = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left")
+ = clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed
%span
= _('Reference:')
@@ -143,11 +142,11 @@
= clipboard_button(text: project_ref, title: _('Copy reference to clipboard'), placement: "left")
- if current_user && issuable.can_move?(current_user)
.block.js-sidebar-move-issue-block
- .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: _('Move issue') }
+ .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= custom_icon('icon_arrow_right')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
- data: { toggle: 'dropdown' } }
+ data: { toggle: 'dropdown', display: 'static' } }
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable
= dropdown_title(_('Move issue'))
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 304df38a096..e1cde527ad7 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -4,7 +4,7 @@
= _('Assignee')
= icon('spinner spin')
- else
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
@@ -13,15 +13,15 @@
= _('Assignee')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- if !signed_in
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
+ %a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
= sidebar_gutter_toggle_icon
.value.hide-collapsed
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do
- if !issuable.can_be_merged_by?(issuable.assignee)
- %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
+ %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= issuable.assignee.to_reference
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index b77e104c072..583b33a8a1b 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -1,11 +1,11 @@
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
-- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark done')
+- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
- todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
- class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
- title: (todo.nil? ? _('Add todo') : _('Mark done')),
- 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark done')),
+ class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'),
+ title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
+ 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) }
%span.issuable-todo-inner.js-issuable-todo-inner<
- if todo
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 9a589387255..fbc96baa0f7 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -6,13 +6,13 @@
%hr
- if issuable.new_record?
- .form-group
- = form.label :source_branch, class: 'control-label'
+ .form-group.row
+ = form.label :source_branch, class: 'col-form-label col-sm-2'
.col-sm-10
.issuable-form-select-holder
= form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 ref-name', disabled: true })
-.form-group
- = form.label :target_branch, class: 'control-label'
+.form-group.row
+ = form.label :target_branch, class: 'col-form-label col-sm-2'
.col-sm-10.target-branch-select-dropdown-container
.issuable-form-select-holder
= form.hidden_field(:target_branch,
diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml
index de508278d7c..f12c23cbc64 100644
--- a/app/views/shared/issuable/form/_contribution.html.haml
+++ b/app/views/shared/issuable/form/_contribution.html.haml
@@ -8,13 +8,13 @@
%hr
.form-group
- .control-label
+ .col-form-label
= _('Contribution')
.col-sm-10
- .checkbox
+ .form-check
= form.label :allow_maintainer_to_push do
= form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user)
= _('Allow edits from maintainers.')
= link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access')
- .help-block
+ .form-text.text-muted
= allow_maintainer_push_unavailable_reason(issuable)
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
index 9b2b6e572e7..0d13fccaf3e 100644
--- a/app/views/shared/issuable/form/_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_issue_assignee.html.haml
@@ -11,7 +11,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if assignees.any?
- assignees.each do |assignee|
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index 8f6509a8ce8..1df881e4102 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -7,10 +7,10 @@
-# This comment and the following line should only exist in CE.
- return unless issuable.can_remove_source_branch?(current_user)
-.form-group
- .col-sm-10.col-sm-offset-2
+.form-group.row
+ .col-sm-10.offset-sm-2
- if issuable.can_remove_source_branch?(current_user)
- .checkbox
+ .form-check
= label_tag 'merge_request[force_remove_source_branch]' do
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
index bf8613b0f0d..05c03dedd91 100644
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -1,6 +1,6 @@
- merge_request = issuable
.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) }
+ .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24)
- else
@@ -9,12 +9,12 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
- unless merge_request.can_be_merged_by?(merge_request.assignee)
- %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+ %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
= icon('exclamation-triangle', 'aria-hidden': 'true')
%span.username
= merge_request.assignee.to_reference
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 1608bd59cf1..1e27253aaeb 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -8,28 +8,28 @@
%hr
.row
- %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
- .form-group.issue-assignee
+ %div{ class: (has_due_date ? "col-lg-6" : "col-12") }
+ .form-group.row.issue-assignee
- if issuable.is_a?(Issue)
= render "shared/issuable/form/metadata_issue_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- else
= render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- .form-group.issue-milestone
- = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
- .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .form-group.row.issue-milestone
+ = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}"
+ .col-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
- .form-group
+ .form-group.row
- has_labels = @labels && @labels.any?
- = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}"
= form.hidden_field :label_ids, multiple: true, value: ''
- .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .col-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
.issuable-form-select-holder
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
- if has_due_date
.col-lg-6
- .form-group
- = form.label :due_date, "Due date", class: "control-label"
- .col-sm-10
+ .form-group.row
+ = form.label :due_date, "Due date", class: "col-form-label col-md-2 col-lg-4"
+ .col-8
.issuable-form-select-holder
= form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
index 567cde764e2..6d4f9ccd66f 100644
--- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
@@ -1,5 +1,5 @@
-= form.label :assignee_ids, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
-.col-sm-10{ class: ("col-lg-8" if has_due_date) }
+= form.label :assignee_ids, "Assignee", class: "col-form-label #{"col-md-2 col-lg-4" if has_due_date}"
+.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { meta: assignee.name, avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }
diff --git a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
index d0ea4e149df..3521f71f409 100644
--- a/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml
@@ -1,4 +1,4 @@
-= form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
= form.hidden_field :assignee_id
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index e81639f35ea..c4f30f5f4d9 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -9,7 +9,7 @@
autocomplete: 'off', class: 'form-control pad qa-issuable-form-title'
- if issuable.respond_to?(:work_in_progress?)
- %p.help-block
+ %p.form-text.text-muted
.js-wip-explanation
%a.js-toggle-wip{ href: '', tabindex: -1 }
Remove the
@@ -30,7 +30,7 @@
merge request from being merged before it's ready.
- if no_issuable_templates && can?(current_user, :push_code, issuable.project)
- %p.help-block
+ %p.form-text.text-muted
Add
= link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
to help your contributors communicate effectively!
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index e8b04f56839..f79f66b144f 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,21 +1,22 @@
-= form_for @label, as: :label, url: url, html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
+= form_for @label, as: :label, url: url, html: { class: 'label-form js-quick-submit js-requires-input' } do |f|
= form_errors(@label)
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :title, class: "form-control", required: true, autofocus: true
- .form-group
- = f.label :description, class: 'control-label'
+ .form-group.row
+ = f.label :description, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :description, class: "form-control js-quick-submit"
- .form-group
- = f.label :color, "Background color", class: 'control-label'
+ .form-group.row
+ = f.label :color, "Background color", class: 'col-form-label col-sm-2'
.col-sm-10
.input-group
- .input-group-addon.label-color-preview &nbsp;
+ .input-group-prepend
+ .input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "form-control"
- .help-block
+ .form-text.text-muted
Choose any color.
%br
Or you can choose one of suggested colors below
diff --git a/app/views/shared/members/_filter_2fa_dropdown.html.haml b/app/views/shared/members/_filter_2fa_dropdown.html.haml
new file mode 100644
index 00000000000..95c35c56b3c
--- /dev/null
+++ b/app/views/shared/members/_filter_2fa_dropdown.html.haml
@@ -0,0 +1,11 @@
+- filter = params[:two_factor] || 'everyone'
+- filter_options = { 'everyone' => 'Everyone', 'enabled' => 'Enabled', 'disabled' => 'Disabled' }
+.dropdown.inline.member-filter-2fa-dropdown
+ = dropdown_toggle('2FA: ' + filter_options[filter], { toggle: 'dropdown' })
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ Filter by two-factor authentication
+ - filter_options.each do |value, title|
+ %li
+ = link_to filter_group_project_member_path(two_factor: value), class: ("is-active" if filter == value) do
+ = title
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index fc634856061..67b8843a27f 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -14,7 +14,7 @@
%span{ class: ('text-warning' if group_link.expires_soon?) }
Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
- = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'form-horizontal js-edit-member-form' do
+ = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
@@ -23,7 +23,7 @@
%span.dropdown-toggle-text
= group_link.human_access
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable
= dropdown_title("Change permissions")
.dropdown-content
%ul
@@ -40,6 +40,6 @@
method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do
- %span.visible-xs-block
+ %span.d-block.d-sm-none
Delete
- = icon('trash', class: 'hidden-xs')
+ = icon('trash', class: 'd-none d-sm-block')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 1c139827acf..42b2d27c44a 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -14,12 +14,16 @@
%span.cgray= user.to_reference
- if user == current_user
- %span.label.label-success.prepend-left-5 It's you
+ %span.badge.badge-success.prepend-left-5 It's you
- if user.blocked?
- %label.label.label-danger
+ %label.badge.badge-danger
%strong Blocked
+ - if user.two_factor_enabled?
+ %label.label.label-info
+ 2FA
+
- if source.instance_of?(Group) && source != @group
&middot;
= link_to source.full_name, source, class: "member-group-link"
@@ -53,11 +57,11 @@
- if member.can_resend_invite?
= link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]),
method: :post,
- class: 'btn btn-default prepend-left-10 hidden-xs',
+ class: 'btn btn-default prepend-left-10 d-none d-sm-block',
title: 'Resend invite'
- if user != current_user && member.can_update?
- = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
+ = form_for member, remote: true, html: { class: 'js-edit-member-form form-horizontal' } do |f|
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
@@ -65,7 +69,7 @@
%span.dropdown-toggle-text
= member.human_access
= icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-align-right.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable
= dropdown_title("Change permissions")
.dropdown-content
%ul
@@ -89,10 +93,10 @@
method: :post,
class: 'btn btn-success prepend-left-10',
title: 'Grant access' do
- %span{ class: ('visible-xs-block' unless force_mobile_view) }
+ %span{ class: ('d-block d-sm-none' unless force_mobile_view) }
Grant access
- unless force_mobile_view
- = icon('check inverse', class: 'hidden-xs')
+ = icon('check inverse', class: 'd-none d-sm-block')
- if member.can_remove?
- if current_user == user
@@ -106,9 +110,9 @@
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
title: remove_member_title(member) do
- %span{ class: ('visible-xs-block' unless force_mobile_view) }
+ %span{ class: ('d-block d-sm-none' unless force_mobile_view) }
Delete
- unless force_mobile_view
- = icon('trash', class: 'hidden-xs')
+ = icon('trash', class: 'd-none d-sm-block')
- else
%span.member-access-text= member.human_access
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
index 1fbd6bcc4cb..54679ab86cc 100644
--- a/app/views/shared/members/_requests.html.haml
+++ b/app/views/shared/members/_requests.html.haml
@@ -4,10 +4,10 @@
- return if requesters.empty?
-.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) }
- .panel-heading
+.card.prepend-top-default{ class: ('card-mobile' if force_mobile_view ) }
+ .card-header
Users requesting access to
%strong= membership_source.name
- %span.badge= requesters.size
+ %span.badge.badge-pill= requesters.size
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view }
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
index bad0891f9f2..56b8c8f033e 100644
--- a/app/views/shared/members/_sort_dropdown.html.haml
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -1,6 +1,6 @@
.dropdown.inline.member-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
Sort by
- member_sort_options_hash.each do |value, title|
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index a74cdbe274b..608dd35182d 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -1,12 +1,11 @@
.col-md-6
- .form-group
- = f.label :start_date, "Start Date", class: "control-label"
+ .form-group.row
+ = f.label :start_date, "Start Date", class: "col-form-label col-sm-2"
.col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
- %a.inline.pull-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
-.col-md-6
- .form-group
- = f.label :due_date, "Due Date", class: "control-label"
+ %a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
+ .form-group.row
+ = f.label :due_date, "Due Date", class: "col-form-label col-sm-2"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
- %a.inline.pull-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
+ %a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 7175e275f95..d8e4d2ff88c 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -1,9 +1,9 @@
- show_counter = local_assigns.fetch(:show_counter, false)
- primary = local_assigns.fetch(:primary, false)
-- panel_class = primary ? 'panel-primary' : 'panel-default'
+- panel_class = primary ? 'bg-primary text-white' : ''
-.panel{ class: panel_class }
- .panel-heading
+.card{ class: panel_class }
+ .card-header
.title
= title
- if show_counter
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index a26b3b8009e..6797520650d 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -10,7 +10,7 @@
%span.prepend-description-left
= markdown_field(label, :description)
- .pull-right.hidden-xs.hidden-sm.hidden-md
+ .float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_label_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do
- pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
= link_to milestones_label_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index ac494814f55..09bbd04c2bf 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -11,7 +11,7 @@
%span - Project Milestone
.col-sm-6
- .pull-right.light #{milestone.percent_complete(current_user)}% complete
+ .float-right.light #{milestone.percent_complete(current_user)}% complete
.row
.col-sm-6
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
@@ -26,19 +26,19 @@
.projects
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
- %span.label.label-gray
+ %span.badge.badge-gray
= dashboard ? milestone.project.full_name : milestone.project.name
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
- if milestone.group_milestone?
- = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do
+ = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-sm btn-grouped" do
Edit
\
- if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
+ = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
- = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-xs btn-grouped btn-close"
+ = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
- if @project
.row
@@ -46,12 +46,12 @@
= render('shared/milestone_expired', milestone: milestone)
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do
+ = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-sm btn-grouped" do
Edit
\
- if @project.group
- %button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
+ %button.js-promote-project-milestone-button.btn.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
disabled: true,
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
@@ -62,9 +62,9 @@
toggle: 'modal' } }
= _('Promote')
- = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
+ = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
- %button.js-delete-milestone-button.btn.btn-xs.btn-grouped.btn-danger{ data: { toggle: 'modal',
+ %button.js-delete-milestone-button.btn.btn-sm.btn-grouped.btn-danger{ data: { toggle: 'modal',
target: '#delete-milestone-modal',
milestone_id: milestone.id,
milestone_title: markdown_field(milestone, :title),
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index ee134480705..95138af3950 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -4,12 +4,8 @@
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
- %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
-
- .sidebar-collapsed-icon
- %span== #{milestone.percent_complete(current_user)}%
- = milestone_progress_bar(milestone)
.title.hide-collapsed
%strong.bold== #{milestone.percent_complete(current_user)}%
%span.hide-collapsed
@@ -17,11 +13,16 @@
.value.hide-collapsed
= milestone_progress_bar(milestone)
+ .block.milestone-progress.hide-expanded
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ %span== #{milestone.percent_complete(current_user)}%
+ = milestone_progress_bar(milestone)
+
.block.start_date.hide-collapsed
.title
Start date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value
%span.value-content
- if milestone.start_date
@@ -35,45 +36,51 @@
%span.collapsed-milestone-date
- if milestone.start_date && milestone.due_date
- if milestone.start_date.year == milestone.due_date.year
- .milestone-date= milestone.start_date.strftime('%b %-d')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ = milestone.start_date.strftime('%b %-d')
- else
- .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ = milestone.start_date.strftime('%b %-d %Y')
.date-separator -
- .due_date= milestone.due_date.strftime('%b %-d %Y')
+ .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ = milestone.due_date.strftime('%b %-d %Y')
- elsif milestone.start_date
From
- .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ = milestone.start_date.strftime('%b %-d %Y')
- elsif milestone.due_date
Until
- .milestone-date= milestone.due_date.strftime('%b %-d %Y')
+ .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ = milestone.due_date.strftime('%b %-d %Y')
- else
- None
+ .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
+ None
.title.hide-collapsed
Due date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
%span.value-content
- if milestone.due_date
%span.bold= milestone.due_date.to_s(:medium)
- else
%span.no-value No due date
- - remaining_days = milestone_remaining_days(milestone)
+ - remaining_days = remaining_days_in_words(milestone)
- if remaining_days.present?
= surround '(', ')' do
%span.remaining-days= remaining_days
- if !project || can?(current_user, :read_issue, project)
.block.issues
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
%strong
= custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
Issues
- %span.badge= milestone.issues_visible_to_user(current_user).count
+ %span.badg.badge-pille= milestone.issues_visible_to_user(current_user).count
- if show_new_issue_link?(project)
- = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do
+ = link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: "New Issue" do
New issue
.value.hide-collapsed.bold
%span.milestone-stat
@@ -93,13 +100,13 @@
= icon('spinner spin')
.block.merge-requests
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
%strong
= custom_icon('mr_bold')
%span= milestone.merge_requests.count
.title.hide-collapsed
Merge requests
- %span.badge= milestone.merge_requests.count
+ %span.badge.badge-pill= milestone.merge_requests.count
.value.hide-collapsed.bold
- if !project || can?(current_user, :read_merge_request, project)
%span.milestone-stat
@@ -129,10 +136,10 @@
- if milestone_ref.present?
.block.reference
.sidebar-collapsed-icon.dont-change-state
- = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
+ = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed
%span
Reference:
%cite{ title: milestone_ref }
= milestone_ref
- = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
+ = clipboard_button(text: milestone_ref, title: "Copy reference to clipboard", placement: "left", boundary: 'viewport')
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index b95a4ea674d..e6a65161ed6 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -3,29 +3,29 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs.js-milestone-tabs
+ %ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs
- if issues_accessible
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
- %span.badge= milestone.issues_visible_to_user(current_user).size
+ %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
- %span.badge= milestone.merge_requests.size
+ %span.badge.badge-pill= milestone.merge_requests.size
- else
%li.active
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
- %span.badge= milestone.merge_requests.size
+ %span.badge.badge-pill= milestone.merge_requests.size
%li
= link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants
- %span.badge= milestone.participants.count
+ %span.badge.badge-pill= milestone.participants.count
%li
= link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels
- %span.badge= milestone.labels.count
+ %span.badge.badge-pill= milestone.labels.count
- issues = milestone.sorted_issues(current_user)
- show_project_name = local_assigns.fetch(:show_project_name, false)
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 797ff034bb2..ee0e35cedc6 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -5,7 +5,7 @@
- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
.detail-page-header
- %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
= icon('angle-double-left')
.status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
@@ -22,7 +22,7 @@
&nbsp;&middot;
= milestone_date_range(milestone)
- if group
- .pull-right
+ .float-right
- if can?(current_user, :admin_milestones, group)
- if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index 4b9af78bc1a..ed336df4e9d 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,6 +1,6 @@
- noteable_name = @note.noteable.human_class_name
-.pull-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
+.float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
%input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
- if @note.can_be_discussion_note?
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index bc1ac3d8ac2..00eae553279 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -32,4 +32,4 @@
= icon('file-image-o', class: 'toolbar-button-icon')
Attach a file
- %button.btn.btn-default.btn-xs.hide.button-cancel-uploading-files{ type: 'button' } Cancel
+ %button.btn.btn-default.btn-sm.hide.button-cancel-uploading-files{ type: 'button' } Cancel
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 893a7f26ebd..ca6e3602f05 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -25,7 +25,7 @@
- elsif note_counter == 0
- counter = badge_counter if local_assigns[:badge_counter]
- badge_class = "hidden" if @fresh_discussion || counter.nil?
- %span.badge{ class: badge_class }
+ %span.badge.badge-pill{ class: badge_class }
= counter
.timeline-content
.note-header
@@ -41,8 +41,9 @@
- if note.system
%span.system-note-message
= markdown_field(note, :note)
- %a{ href: "##{dom_id(note)}" }
- = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
+ %span.system-note-separator
+ &middot;
+ %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- if note.for_personal_snippet?
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 1db7c4e67cf..b98d5339d2d 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -13,7 +13,7 @@
.timeline-entry-inner
.flash-container.timeline-content
- .timeline-icon.hidden-xs.hidden-sm
+ .timeline-icon.d-none.d-sm-none.d-md-block
%a.author_link{ href: user_path(current_user) }
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 9186c2ba9c9..d830225d169 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -22,7 +22,7 @@
- NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
.form-group
- .checkbox{ class: ("prepend-top-0" if index == 0) }
+ .form-check{ class: ("prepend-top-0" if index == 0) }
%label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event))
%strong
diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml
index fc643c3ecc2..7bcc54e7459 100644
--- a/app/views/shared/plugins/_index.html.haml
+++ b/app/views/shared/plugins/_index.html.haml
@@ -10,8 +10,8 @@
.col-lg-8.append-bottom-default
- if plugins.any?
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
Plugins (#{plugins.count})
%ul.content-list
- plugins.each do |file|
@@ -19,5 +19,5 @@
.monospace
= File.basename(file)
- else
- %p.light-well.text-center
+ %p.card.bg-light.text-center
No plugins found.
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 3d917346f6b..98b258d9275 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,8 +1,8 @@
- @sort ||= sort_value_latest_activity
.dropdown.js-project-filter-dropdown-wrap
- toggle_text = projects_sort_options_hash[@sort]
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ = dropdown_toggle(toggle_text, { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' })
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
Sort by
- projects_sort_options_hash.each do |value, title|
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 0687f6d961d..88f0675f795 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -46,7 +46,7 @@
.controls
.prepend-top-0
- if project.archived
- %span.prepend-left-10.label.label-warning archived
+ %span.prepend-left-10.badge.badge-warning archived
- if can?(current_user, :read_cross_project) && project.pipeline_status.has_status?
%span.prepend-left-10
= render_project_pipeline_status(project.pipeline_status)
diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
new file mode 100644
index 00000000000..660123d8b07
--- /dev/null
+++ b/app/views/shared/runners/_form.html.haml
@@ -0,0 +1,56 @@
+= form_for runner, url: runner_form_url do |f|
+ = form_errors(runner)
+ .form-group.row
+ = label :active, "Active", class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.check_box :active
+ %span.light Paused Runners don't accept new jobs
+ .form-group.row
+ = label :protected, "Protected", class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.check_box :access_level, {}, 'ref_protected', 'not_protected'
+ %span.light This runner will only run on pipelines triggered on protected branches
+ .form-group.row
+ = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.check_box :run_untagged
+ %span.light Indicates whether this runner can pick jobs without tags
+ - unless runner.group_type?
+ .form-group.row
+ = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2'
+ .col-sm-10
+ .form-check
+ = f.check_box :locked
+ %span.light= _('When a runner is locked, it cannot be assigned to other projects')
+ .form-group.row
+ = label_tag :token, class: 'col-form-label col-sm-2' do
+ Token
+ .col-sm-10
+ = f.text_field :token, class: 'form-control', readonly: true
+ .form-group.row
+ = label_tag :ip_address, class: 'col-form-label col-sm-2' do
+ IP Address
+ .col-sm-10
+ = f.text_field :ip_address, class: 'form-control', readonly: true
+ .form-group.row
+ = label_tag :description, class: 'col-form-label col-sm-2' do
+ Description
+ .col-sm-10
+ = f.text_field :description, class: 'form-control'
+ .form-group.row
+ = label_tag :maximum_timeout_human_readable, class: 'col-form-label col-sm-2' do
+ Maximum job timeout
+ .col-sm-10
+ = f.text_field :maximum_timeout_human_readable, class: 'form-control'
+ .form-text.text-muted This timeout will take precedence when lower than Project-defined timeout
+ .form-group.row
+ = label_tag :tag_list, class: 'col-form-label col-sm-2' do
+ Tags
+ .col-sm-10
+ = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
+ .form-text.text-muted You can setup jobs to only use Runners with specific tags. Separate tags with commas.
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml
new file mode 100644
index 00000000000..da5c032add5
--- /dev/null
+++ b/app/views/shared/runners/_runner_description.html.haml
@@ -0,0 +1,16 @@
+.light.prepend-top-default
+ %p
+ = _("A 'Runner' is a process which runs a job. You can setup as many Runners as you need.")
+ %br
+ = _('Runners can be placed on separate users, servers, and even on your local machine.')
+
+ %p
+ = _('Each Runner can be in one of the following states:')
+ %div
+ %ul
+ %li
+ %span.badge.badge-success active
+ = _('- Runner is active and can process any new jobs')
+ %li
+ %span.badge.badge-danger paused
+ = _('- Runner is paused and will not receive any new jobs')
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
new file mode 100644
index 00000000000..e50b7fa68dd
--- /dev/null
+++ b/app/views/shared/runners/show.html.haml
@@ -0,0 +1,71 @@
+- page_title "#{@runner.description} ##{@runner.id}", "Runners"
+
+%h3.page-title
+ Runner ##{@runner.id}
+ .float-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - elsif @runner.group_type?
+ %span.runner-state.runner-state-shared
+ Group
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
+
+.table-holder
+ %table.table
+ %thead
+ %tr
+ %th Property Name
+ %th Value
+ %tr
+ %td Active
+ %td= @runner.active? ? _('Yes') : _('No')
+ %tr
+ %td Protected
+ %td= @runner.ref_protected? ? _('Yes') : _('No')
+ %tr
+ %td= _('Can run untagged jobs')
+ %td= @runner.run_untagged? ? _('Yes') : _('No')
+ - unless @runner.group_type?
+ %tr
+ %td= _('Locked to this project')
+ %td= @runner.locked? ? _('Yes') : _('No')
+ %tr
+ %td Tags
+ %td
+ - @runner.tag_list.sort.each do |tag|
+ %span.badge.badge-primary
+ = tag
+ %tr
+ %td Name
+ %td= @runner.name
+ %tr
+ %td Version
+ %td= @runner.version
+ %tr
+ %td IP Address
+ %td= @runner.ip_address
+ %tr
+ %td Revision
+ %td= @runner.revision
+ %tr
+ %td Platform
+ %td= @runner.platform
+ %tr
+ %td Architecture
+ %td= @runner.architecture
+ %tr
+ %td Description
+ %td= @runner.description
+ %tr
+ %td= _('Maximum job timeout')
+ %td= @runner.maximum_timeout_human_readable
+ %tr
+ %td Last contact
+ %td
+ - if @runner.contacted_at
+ = time_ago_with_tooltip @runner.contacted_at
+ - else
+ Never
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 11f0fa7c49f..2132fcbccc5 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -2,7 +2,7 @@
.js-file-title.file-title-flex-parent
= render 'projects/blob/header_content', blob: blob
- .file-actions.hidden-xs
+ .file-actions.d-none.d-sm-block
= render 'projects/blob/viewer_switcher', blob: blob
.btn-group{ role: "group" }<
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
index 2d93e51a2d9..36f56fbad1a 100644
--- a/app/views/shared/snippets/_embed.html.haml
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -15,7 +15,7 @@
%span.logo-text
GitLab
- .file-actions.hidden-xs
+ .file-actions.d-none.d-sm-block
.btn-group{ role: "group" }<
= embedded_snippet_raw_button
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index c75c882a693..858adc8be37 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -2,11 +2,11 @@
= page_specific_javascript_tag('lib/ace.js')
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
+ = form_for @snippet, url: url, html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
= form_errors(@snippet)
- .form-group
- = f.label :title, class: 'control-label'
+ .form-group.row
+ = f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :title, class: 'form-control', required: true, autofocus: true
@@ -15,8 +15,8 @@
= render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet
.file-editor
- .form-group
- = f.label :file_name, "File", class: 'control-label'
+ .form-group.row
+ = f.label :file_name, "File", class: 'col-form-label col-sm-2'
.col-sm-10
.file-holder.snippet
.js-file-title.file-title
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 836230ae8ee..ddf54866b99 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -7,7 +7,7 @@
%span.creator
Authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
+ by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "d-none d-sm-inline")}
.detail-page-header-actions
- if @snippet.project_id?
@@ -45,6 +45,6 @@
%strong.embed-toggle-list-item= _("Share")
%input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
.input-group-btn
- %button.js-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '#snippet-url-area' }
+ %button.js-clipboard-btn.snippet-clipboard-btn.btn.btn-default.has-tooltip{ title: "Copy to clipboard", 'data-clipboard-target': '.js-snippet-url-area' }
= sprite_icon('duplicate', size: 16)
.clearfix
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 3acec88c2e3..e036b21b23f 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,13 +1,13 @@
- link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row
- = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 hidden-xs", alt: ''
+ = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- if snippet.file_name
- %span.snippet-filename.monospace.hidden-xs
+ %span.snippet-filename.monospace.d-none.d-sm-inline-block
= snippet.file_name
%ul.controls
@@ -28,10 +28,10 @@
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
- if link_project && snippet.project_id?
- %span.hidden-xs
+ %span.d-none.d-sm-block
in
= link_to project_path(snippet.project) do
= snippet.project.full_name
- .pull-right.snippet-updated-at
+ .float-right.snippet-updated-at
%span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index d36ca032558..0d1c007dd78 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -6,76 +6,76 @@
.form-group
= form.label :token, 'Secret Token', class: 'label-light'
= form.text_field :token, class: 'form-control', placeholder: ''
- %p.help-block
+ %p.form-text.text-muted
Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
.form-group
= form.label :url, 'Trigger', class: 'label-light'
%ul.list-unstyled
%li
- = form.check_box :push_events, class: 'pull-left'
+ = form.check_box :push_events, class: 'float-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered by a push to the repository
%li
- = form.check_box :tag_push_events, class: 'pull-left'
+ = form.check_box :tag_push_events, class: 'float-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This URL will be triggered when a new tag is pushed to the repository
%li
- = form.check_box :note_events, class: 'pull-left'
+ = form.check_box :note_events, class: 'float-left'
.prepend-left-20
= form.label :note_events, class: 'list-label' do
%strong Comments
%p.light
This URL will be triggered when someone adds a comment
%li
- = form.check_box :confidential_note_events, class: 'pull-left'
+ = form.check_box :confidential_note_events, class: 'float-left'
.prepend-left-20
= form.label :confidential_note_events, class: 'list-label' do
%strong Confidential Comments
%p.light
This URL will be triggered when someone adds a comment on a confidential issue
%li
- = form.check_box :issues_events, class: 'pull-left'
+ = form.check_box :issues_events, class: 'float-left'
.prepend-left-20
= form.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
This URL will be triggered when an issue is created/updated/merged
%li
- = form.check_box :confidential_issues_events, class: 'pull-left'
+ = form.check_box :confidential_issues_events, class: 'float-left'
.prepend-left-20
= form.label :confidential_issues_events, class: 'list-label' do
%strong Confidential Issues events
%p.light
This URL will be triggered when a confidential issue is created/updated/merged
%li
- = form.check_box :merge_requests_events, class: 'pull-left'
+ = form.check_box :merge_requests_events, class: 'float-left'
.prepend-left-20
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge request events
%p.light
This URL will be triggered when a merge request is created/updated/merged
%li
- = form.check_box :job_events, class: 'pull-left'
+ = form.check_box :job_events, class: 'float-left'
.prepend-left-20
= form.label :job_events, class: 'list-label' do
%strong Job events
%p.light
This URL will be triggered when the job status changes
%li
- = form.check_box :pipeline_events, class: 'pull-left'
+ = form.check_box :pipeline_events, class: 'float-left'
.prepend-left-20
= form.label :pipeline_events, class: 'list-label' do
%strong Pipeline events
%p.light
This URL will be triggered when the pipeline status changes
%li
- = form.check_box :wiki_page_events, class: 'pull-left'
+ = form.check_box :wiki_page_events, class: 'float-left'
.prepend-left-20
= form.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events
@@ -83,7 +83,7 @@
This URL will be triggered when a wiki page is created/updated
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
- .checkbox
+ .form-check
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
diff --git a/app/views/shared/web_hooks/_test_button.html.haml b/app/views/shared/web_hooks/_test_button.html.haml
index cf1d5e061c6..5ece8b1d4c7 100644
--- a/app/views/shared/web_hooks/_test_button.html.haml
+++ b/app/views/shared/web_hooks/_test_button.html.haml
@@ -6,7 +6,7 @@
%button.btn{ 'data-toggle' => 'dropdown', class: button_class }
Test
= icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- triggers.each_value do |event|
%li
= link_to_test_hook(hook, event)
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index 1a6e2542dc1..7255d352775 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -4,7 +4,7 @@
- header_title t('sherlock.title'), sherlock_transactions_path
.row-content-block
- .pull-right
+ .float-right
= link_to(sherlock_transaction_path(@transaction), class: 'btn') do
%i.fa.fa-arrow-left
= t('sherlock.transaction')
diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml
index 30e956e5f40..4f5146cefb9 100644
--- a/app/views/sherlock/queries/_backtrace.html.haml
+++ b/app/views/sherlock/queries/_backtrace.html.haml
@@ -1,6 +1,6 @@
.prepend-top-default
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.application_backtrace')
%ul.well-list
@@ -15,8 +15,8 @@
= t('sherlock.line')
= location.line
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.full_backtrace')
%ul.well-list
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index 5a447f791dc..34c0cc4da39 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -1,6 +1,6 @@
.prepend-top-default
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.general')
%ul.well-list
@@ -23,10 +23,10 @@
= t('sherlock.line')
= frame.line
- .panel.panel-default
- .panel-heading
- .pull-right
- %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
+ .card
+ .card-header
+ .float-right
+ %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.formatted_query
@@ -38,10 +38,10 @@
:preserve
#{highlight("#{@query.id}.sql", @query.formatted_query)}
- .panel.panel-default
- .panel-heading
- .pull-right
- %button.js-clipboard-trigger.btn.btn-xs{ title: t('sherlock.copy_to_clipboard'), type: :button }
+ .card
+ .card-header
+ .float-right
+ %button.js-clipboard-trigger.btn.btn-sm{ title: t('sherlock.copy_to_clipboard'), type: :button }
%i.fa.fa-clipboard
%pre.hidden
= @query.explain
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index c45da6ee9a4..413130a2907 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.nav-links
+%ul.nav-links.nav.nav-tabs
%li.active
%a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
@@ -10,7 +10,7 @@
= t('sherlock.backtrace')
.row-content-block
- .pull-right
+ .float-right
= link_to(sherlock_transaction_path(@transaction), class: 'btn') do
%i.fa.fa-arrow-left
= t('sherlock.transaction')
diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml
index 4349c9b7ace..5b3448605f2 100644
--- a/app/views/sherlock/transactions/_file_samples.html.haml
+++ b/app/views/sherlock/transactions/_file_samples.html.haml
@@ -21,4 +21,4 @@
%td
= link_to(t('sherlock.view'),
sherlock_transaction_file_sample_path(@transaction, sample),
- class: 'btn btn-xs')
+ class: 'btn btn-sm')
diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml
index 8533b130da6..7ec8dde8421 100644
--- a/app/views/sherlock/transactions/_general.html.haml
+++ b/app/views/sherlock/transactions/_general.html.haml
@@ -1,6 +1,6 @@
.prepend-top-default
- .panel.panel-default
- .panel-heading
+ .card
+ .card-header
%strong
= t('sherlock.general')
%ul.well-list
@@ -35,5 +35,4 @@
%span.light
#{t('sherlock.finished_at')}:
%strong
- = time_ago_in_words(@transaction.finished_at)
- = t('sherlock.ago')
+ = time_ago_with_tooltip @transaction.finished_at
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
index b8d93e9ff45..c1ec4b91bb6 100644
--- a/app/views/sherlock/transactions/_queries.html.haml
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -21,4 +21,4 @@
%td
= link_to(t('sherlock.view'),
sherlock_transaction_query_path(@transaction, query),
- class: 'btn btn-xs')
+ class: 'btn btn-sm')
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
index bc05659dfa8..4d9df01ae31 100644
--- a/app/views/sherlock/transactions/index.html.haml
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -2,7 +2,7 @@
- header_title t('sherlock.title'), sherlock_transactions_path
.row-content-block
- .pull-right
+ .float-right
= link_to(destroy_all_sherlock_transactions_path,
class: 'btn btn-danger',
method: :delete) do
@@ -35,8 +35,7 @@
= t('sherlock.seconds')
%td= trans.queries.length
%td
- = time_ago_in_words(trans.finished_at)
- = t('sherlock.ago')
+ = time_ago_with_tooltip trans.finished_at
%td
- = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do
+ = link_to(sherlock_transaction_path(trans), class: 'btn btn-sm') do
= t('sherlock.view')
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index eab91e8fbe4..565b337d446 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -1,23 +1,23 @@
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.nav-links
+%ul.nav-links.nav.nav-tabs
%li.active
%a{ href: "#tab-general", data: { toggle: "tab" } }
= t('sherlock.general')
%li
%a{ href: "#tab-queries", data: { toggle: "tab" } }
= t('sherlock.queries')
- %span.badge
+ %span.badge.badge-pill
#{@transaction.queries.length}
%li
%a{ href: "#tab-file-samples", data: { toggle: "tab" } }
= t('sherlock.file_samples')
- %span.badge
+ %span.badge.badge-pill
#{@transaction.file_samples.length}
.row-content-block
- .pull-right
+ .float-right
= link_to(sherlock_transactions_path, class: 'btn') do
%i.fa.fa-arrow-left
= t('sherlock.all_transactions')
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index a7f118d3f7d..ae69d0d07c7 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,6 +1,6 @@
- return unless current_user
-.hidden-xs
+.d-none.d-sm-block
- if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit
@@ -11,7 +11,7 @@
New snippet
- if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
-.visible-xs-block.dropdown
+.d-block.d-sm-none.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
= icon('caret-down')
diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml
index 65aa4fbc757..dc4b0fd9ba0 100644
--- a/app/views/snippets/_snippets_scope_menu.html.haml
+++ b/app/views/snippets/_snippets_scope_menu.html.haml
@@ -1,11 +1,11 @@
- subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false)
-.nav-links.snippet-scope-menu.mobile-separator
+.nav-links.snippet-scope-menu.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do
All
- %span.badge
+ %span.badge.badge-pill
- if include_private
= subject.snippets.count
- else
@@ -15,17 +15,17 @@
%li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
Private
- %span.badge
+ %span.badge.badge-pill
= subject.snippets.are_private.count
%li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
Internal
- %span.badge
+ %span.badge.badge-pill
= subject.snippets.are_internal.count
%li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
Public
- %span.badge
+ %span.badge.badge-pill
= subject.snippets.are_public.count
diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml
index 7e4918a6085..9b4a7dbe68d 100644
--- a/app/views/snippets/index.html.haml
+++ b/app/views/snippets/index.html.haml
@@ -1,12 +1,12 @@
- page_title "By #{@user.name}", "Snippets"
%ol.breadcrumb
- %li
+ %li.breadcrumb-item
= link_to snippets_path do
Snippets
- %li
+ %li.breadcrumb-item
= @user.name
- .pull-right.hidden-xs
+ .float-right.d-none.d-sm-block
= link_to user_path(@user) do
#{@user.name} profile page
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 4bf01ecb48c..b2ec7166832 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -12,7 +12,7 @@
.cover-block.user-cover-block.top-area
.cover-controls
- if @user == current_user
- = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
+ = link_to profile_path, class: 'btn btn-default has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do
= icon('pencil')
- elsif current_user
- if @user.abuse_report
@@ -20,13 +20,13 @@
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
= icon('exclamation-circle')
- else
- = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray',
+ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn',
title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- = link_to user_path(@user, rss_url_options), class: 'btn btn-gray has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
+ = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do
= icon('rss')
- if current_user && current_user.admin?
- = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area',
+ = link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('users')
@@ -35,7 +35,7 @@
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 90), class: "avatar s90", alt: ''
- .user-info
+ .user-info.prepend-left-default.append-right-default
.cover-title
= @user.name
@@ -81,7 +81,7 @@
.scrolling-tabs-container
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.user-profile-nav.scrolling-tabs
+ %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs
- if profile_tab?(:activity)
%li.js-activity-tab
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
@@ -107,7 +107,7 @@
.tab-content
- if profile_tab?(:activity)
#activity.tab-pane
- .row-content-block.calender-block.white.second-block.hidden-xs
+ .row-content-block.calender-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
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
new file mode 100644
index 00000000000..c5406696bdd
--- /dev/null
+++ b/app/views/users/terms/index.html.haml
@@ -0,0 +1,13 @@
+- redirect_params = { redirect: @redirect } if @redirect
+
+.panel-content.rendered-terms
+ = markdown_field(@term, :terms)
+.row-content-block.footer-block.clearfix
+ - if can?(current_user, :accept_terms, @term)
+ .pull-right
+ = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
+ = _('Accept terms')
+ - if can?(current_user, :decline_terms, @term)
+ .pull-right
+ = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
+ = _('Decline and sign out')
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index bec0a003a1c..044e470141e 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -3,6 +3,12 @@ class AdminEmailWorker
include CronjobQueue
def perform
+ send_repository_check_mail if Gitlab::CurrentSettings.repository_checks_enabled
+ end
+
+ private
+
+ def send_repository_check_mail
repository_check_failed_count = Project.where(last_repository_check_failed: true).count
return if repository_check_failed_count.zero?
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 9aea3bad27b..d07865a4043 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -41,6 +41,7 @@
- github_importer:github_import_stage_import_repository
- mail_scheduler:mail_scheduler_issue_due
+- mail_scheduler:mail_scheduler_notification_service
- object_storage_upload
- object_storage:object_storage_background_move
@@ -51,6 +52,7 @@
- pipeline_creation:create_pipeline
- pipeline_creation:run_pipeline_schedule
- pipeline_background:archive_trace
+- pipeline_background:ci_build_trace_chunk_flush
- pipeline_default:build_coverage
- pipeline_default:build_trace_sections
- pipeline_default:pipeline_metrics
@@ -105,9 +107,12 @@
- rebase
- repository_fork
- repository_import
+- repository_remove_remote
- storage_migrator
- system_hook_push
- update_merge_requests
- update_user_activity
- upload_checksum
- web_hook
+- repository_update_remote_mirror
+- create_note_diff_file
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
new file mode 100644
index 00000000000..218d6688bd9
--- /dev/null
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -0,0 +1,12 @@
+module Ci
+ class BuildTraceChunkFlushWorker
+ include ApplicationWorker
+ include PipelineBackgroundQueue
+
+ def perform(build_trace_chunk_id)
+ ::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
+ build_trace_chunk.use_database!
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/mail_scheduler_queue.rb b/app/workers/concerns/mail_scheduler_queue.rb
index 9df55ad9522..f3e9680d756 100644
--- a/app/workers/concerns/mail_scheduler_queue.rb
+++ b/app/workers/concerns/mail_scheduler_queue.rb
@@ -4,4 +4,8 @@ module MailSchedulerQueue
included do
queue_namespace :mail_scheduler
end
+
+ def notification_service
+ @notification_service ||= NotificationService.new
+ end
end
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
new file mode 100644
index 00000000000..624b638a24e
--- /dev/null
+++ b/app/workers/create_note_diff_file_worker.rb
@@ -0,0 +1,9 @@
+class CreateNoteDiffFileWorker
+ include ApplicationWorker
+
+ def perform(diff_note_id)
+ diff_note = DiffNote.find(diff_note_id)
+
+ diff_note.create_diff_file
+ end
+end
diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb
index f7f498af840..8d708e15a66 100644
--- a/app/workers/gitlab/github_import/advance_stage_worker.rb
+++ b/app/workers/gitlab/github_import/advance_stage_worker.rb
@@ -63,11 +63,10 @@ module Gitlab
end
def find_project(id)
- # We only care about the import JID so we can refresh it. We also only
- # want the project if it hasn't been marked as failed yet. It's possible
- # the import gets marked as stuck when jobs of the current stage failed
- # somehow.
- Project.select(:import_jid).import_started.find_by(id: id)
+ # TODO: Only select the JID
+ # This is due to the fact that the JID could be present in either the project record or
+ # its associated import_state record
+ Project.import_started.find_by(id: id)
end
end
end
diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
index 7108b531bc2..68d2c5c4331 100644
--- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
+++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb
@@ -31,7 +31,10 @@ module Gitlab
end
def find_project(id)
- Project.select(:import_jid).import_started.find_by(id: id)
+ # TODO: Only select the JID
+ # This is due to the fact that the JID could be present in either the project record or
+ # its associated import_state record
+ Project.import_started.find_by(id: id)
end
end
end
diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb
index b06079d68ca..54285884a52 100644
--- a/app/workers/mail_scheduler/issue_due_worker.rb
+++ b/app/workers/mail_scheduler/issue_due_worker.rb
@@ -4,8 +4,6 @@ module MailScheduler
include MailSchedulerQueue
def perform(project_id)
- notification_service = NotificationService.new
-
Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
notification_service.issue_due(issue)
end
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
new file mode 100644
index 00000000000..7cfe0aa0df1
--- /dev/null
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -0,0 +1,19 @@
+require 'active_job/arguments'
+
+module MailScheduler
+ class NotificationServiceWorker
+ include ApplicationWorker
+ include MailSchedulerQueue
+
+ def perform(meth, *args)
+ deserialized_args = ActiveJob::Arguments.deserialize(args)
+
+ notification_service.public_send(meth, *deserialized_args) # rubocop:disable GitlabSecurity/PublicSend
+ rescue ActiveJob::DeserializationError
+ end
+
+ def self.perform_async(*args)
+ super(*ActiveJob::Arguments.serialize(args))
+ end
+ end
+end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index b925741934a..67c54fbf10e 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -5,7 +5,7 @@ class NewNoteWorker
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id)
- NotificationService.new.new_note(note) if note.can_create_notification?
+ NotificationService.new.new_note(note)
Notes::PostProcessService.new(note).execute
else
Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index a6b2c251254..a3ecfa8e711 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -9,85 +9,6 @@ module ObjectStorage
SanityCheckError = Class.new(StandardError)
- class Upload < ActiveRecord::Base
- # Upper limit for foreground checksum processing
- CHECKSUM_THRESHOLD = 100.megabytes
-
- belongs_to :model, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
-
- validates :size, presence: true
- validates :path, presence: true
- validates :model, presence: true
- validates :uploader, presence: true
-
- before_save :calculate_checksum!, if: :foreground_checksummable?
- after_commit :schedule_checksum, if: :checksummable?
-
- scope :stored_locally, -> { where(store: [nil, ObjectStorage::Store::LOCAL]) }
- scope :stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
-
- def self.hexdigest(path)
- Digest::SHA256.file(path).hexdigest
- end
-
- def absolute_path
- raise ObjectStorage::RemoteStoreError, "Remote object has no absolute path." unless local?
- return path unless relative_path?
-
- uploader_class.absolute_path(self)
- end
-
- def calculate_checksum!
- self.checksum = nil
- return unless checksummable?
-
- self.checksum = self.class.hexdigest(absolute_path)
- end
-
- def build_uploader(mounted_as = nil)
- uploader_class.new(model, mounted_as).tap do |uploader|
- uploader.upload = self
- uploader.retrieve_from_store!(identifier)
- end
- end
-
- def exist?
- File.exist?(absolute_path)
- end
-
- def local?
- return true if store.nil?
-
- store == ObjectStorage::Store::LOCAL
- end
-
- private
-
- def checksummable?
- checksum.nil? && local? && exist?
- end
-
- def foreground_checksummable?
- checksummable? && size <= CHECKSUM_THRESHOLD
- end
-
- def schedule_checksum
- UploadChecksumWorker.perform_async(id)
- end
-
- def relative_path?
- !path.start_with?('/')
- end
-
- def identifier
- File.basename(path)
- end
-
- def uploader_class
- Object.const_get(uploader)
- end
- end
-
class MigrationResult
attr_reader :upload
attr_accessor :error
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 76688cf51c1..72f0a9b0619 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -4,8 +4,11 @@ module RepositoryCheck
include CronjobQueue
RUN_TIME = 3600
+ BATCH_SIZE = 10_000
def perform
+ return unless Gitlab::CurrentSettings.repository_checks_enabled
+
start = Time.now
# This loop will break after a little more than one hour ('a little
@@ -15,7 +18,6 @@ module RepositoryCheck
# check, only one (or two) will be checked at a time.
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
- break unless current_settings.repository_checks_enabled
next unless try_obtain_lease(project_id)
@@ -31,12 +33,20 @@ module RepositoryCheck
# getting ID's from Postgres is not terribly slow, and because no user
# has to sit and wait for this query to finish.
def project_ids
- limit = 10_000
- never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago)
- .limit(limit).pluck(:id)
- old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago)
- .reorder('last_repository_check_at ASC').limit(limit).pluck(:id)
- never_checked_projects + old_check_projects
+ never_checked_project_ids(BATCH_SIZE) + old_checked_project_ids(BATCH_SIZE)
+ end
+
+ def never_checked_project_ids(batch_size)
+ Project.where(last_repository_check_at: nil)
+ .where('created_at < ?', 24.hours.ago)
+ .limit(batch_size).pluck(:id)
+ end
+
+ def old_checked_project_ids(batch_size)
+ Project.where.not(last_repository_check_at: nil)
+ .where('last_repository_check_at < ?', 1.month.ago)
+ .reorder(last_repository_check_at: :asc)
+ .limit(batch_size).pluck(:id)
end
def try_obtain_lease(id)
@@ -47,16 +57,5 @@ module RepositoryCheck
timeout: 24.hours
).try_obtain
end
-
- def current_settings
- # No caching of the settings! If we cache them and an admin disables
- # this feature, an active RepositoryCheckWorker would keep going for up
- # to 1 hour after the feature was disabled.
- if Rails.env.test?
- Gitlab::CurrentSettings.fake_application_settings
- else
- ApplicationSetting.current
- end
- end
end
end
diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb
index 116bc185b38..3cffb8b14e4 100644
--- a/app/workers/repository_check/single_repository_worker.rb
+++ b/app/workers/repository_check/single_repository_worker.rb
@@ -5,27 +5,34 @@ module RepositoryCheck
def perform(project_id)
project = Project.find(project_id)
+ healthy = project_healthy?(project)
+
+ update_repository_check_status(project, healthy)
+ end
+
+ private
+
+ def update_repository_check_status(project, healthy)
project.update_columns(
- last_repository_check_failed: !check(project),
+ last_repository_check_failed: !healthy,
last_repository_check_at: Time.now
)
end
- private
+ def project_healthy?(project)
+ repo_healthy?(project) && wiki_repo_healthy?(project)
+ end
- def check(project)
- if has_pushes?(project) && !git_fsck(project.repository)
- false
- elsif project.wiki_enabled?
- # Historically some projects never had their wiki repos initialized;
- # this happens on project creation now. Let's initialize an empty repo
- # if it is not already there.
- project.create_wiki
+ def repo_healthy?(project)
+ return true unless has_changes?(project)
- git_fsck(project.wiki.repository)
- else
- true
- end
+ git_fsck(project.repository)
+ end
+
+ def wiki_repo_healthy?(project)
+ return true unless has_wiki_changes?(project)
+
+ git_fsck(project.wiki.repository)
end
def git_fsck(repository)
@@ -39,8 +46,19 @@ module RepositoryCheck
false
end
- def has_pushes?(project)
+ def has_changes?(project)
Project.with_push.exists?(project.id)
end
+
+ def has_wiki_changes?(project)
+ return false unless project.wiki_enabled?
+
+ # Historically some projects never had their wiki repos initialized;
+ # this happens on project creation now. Let's initialize an empty repo
+ # if it is not already there.
+ return false unless project.create_wiki
+
+ has_changes?(project)
+ end
end
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 51fad4faf36..08b1c3a7d7a 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -13,7 +13,9 @@ class RepositoryForkWorker
# See https://gitlab.com/gitlab-org/gitaly/issues/1110
if args.empty?
source_project = target_project.forked_from_project
- return target_project.mark_import_as_failed('Source project cannot be found.') unless source_project
+ unless source_project
+ return target_project.mark_import_as_failed('Source project cannot be found.')
+ end
fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
else
diff --git a/app/workers/repository_remove_remote_worker.rb b/app/workers/repository_remove_remote_worker.rb
new file mode 100644
index 00000000000..1c19b604b77
--- /dev/null
+++ b/app/workers/repository_remove_remote_worker.rb
@@ -0,0 +1,35 @@
+class RepositoryRemoveRemoteWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
+
+ attr_reader :project, :remote_name
+
+ def perform(project_id, remote_name)
+ @remote_name = remote_name
+ @project = Project.find_by_id(project_id)
+
+ return unless @project
+
+ logger.info("Removing remote #{remote_name} from project #{project.id}")
+
+ try_obtain_lease do
+ remove_remote = @project.repository.remove_remote(remote_name)
+
+ if remove_remote
+ logger.info("Remote #{remote_name} was successfully removed from project #{project.id}")
+ else
+ logger.error("Could not remove remote #{remote_name} from project #{project.id}")
+ end
+ end
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ def lease_key
+ "remove_remote_#{project.id}_#{remote_name}"
+ end
+end
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
new file mode 100644
index 00000000000..bb963979e88
--- /dev/null
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -0,0 +1,49 @@
+class RepositoryUpdateRemoteMirrorWorker
+ UpdateAlreadyInProgressError = Class.new(StandardError)
+ UpdateError = Class.new(StandardError)
+
+ include ApplicationWorker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options retry: 3, dead: false
+
+ sidekiq_retry_in { |count| 30 * count }
+
+ sidekiq_retries_exhausted do |msg, _|
+ Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
+ end
+
+ def perform(remote_mirror_id, scheduled_time)
+ remote_mirror = RemoteMirror.find(remote_mirror_id)
+ return if remote_mirror.updated_since?(scheduled_time)
+
+ raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress?
+
+ remote_mirror.update_start
+
+ project = remote_mirror.project
+ current_user = project.creator
+ result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror)
+ raise UpdateError, result[:message] if result[:status] == :error
+
+ remote_mirror.update_finish
+ rescue UpdateAlreadyInProgressError
+ raise
+ rescue UpdateError => ex
+ fail_remote_mirror(remote_mirror, ex.message)
+ raise
+ rescue => ex
+ return unless remote_mirror
+
+ fail_remote_mirror(remote_mirror, ex.message)
+ raise UpdateError, "#{ex.class}: #{ex.message}"
+ end
+
+ private
+
+ def fail_remote_mirror(remote_mirror, message)
+ remote_mirror.mark_as_failed(message)
+
+ Rails.logger.error(message)
+ end
+end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index fbb14efc525..6fdd7592e74 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -22,7 +22,8 @@ class StuckImportJobsWorker
end
def mark_projects_with_jid_as_failed!
- jids_and_ids = enqueued_projects_with_jid.pluck(:import_jid, :id).to_h
+ # 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
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
@@ -42,15 +43,15 @@ class StuckImportJobsWorker
end
def enqueued_projects
- Project.with_import_status(:scheduled, :started)
+ Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')")
end
def enqueued_projects_with_jid
- enqueued_projects.where.not(import_jid: nil)
+ enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
def enqueued_projects_without_jid
- enqueued_projects.where(import_jid: nil)
+ enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL")
end
def error_message
diff --git a/bin/secpick b/bin/secpick
index 76ae231e913..5029fe57cfe 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -5,7 +5,6 @@ require 'rainbow/refinement'
using Rainbow
BRANCH_PREFIX = 'security'.freeze
-STABLE_BRANCH_SUFFIX = 'stable'.freeze
REMOTE = 'dev'.freeze
options = { version: nil, branch: nil, sha: nil }
@@ -37,9 +36,9 @@ abort("Missing options. Use #{$0} --help to see the list of options available".r
abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze
-stable_branch = "#{options[:version]}-#{STABLE_BRANCH_SUFFIX}".freeze
+stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze
-command = "git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
+command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
_stdin, stdout, stderr = Open3.popen3(command)
diff --git a/bin/spinach b/bin/spinach
deleted file mode 100755
index eda81c9ed8a..00000000000
--- a/bin/spinach
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env ruby
-
-# Remove this block when removing rails5? code.
-gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile"
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__)
-
-begin
- load File.expand_path('../spring', __FILE__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
-end
-require 'bundler/setup'
-load Gem.bin_path('spinach', 'spinach')
diff --git a/changelogs/unreleased/16957-issue-due-email.yml b/changelogs/unreleased/16957-issue-due-email.yml
deleted file mode 100644
index 83944ca4f73..00000000000
--- a/changelogs/unreleased/16957-issue-due-email.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add cron job to email users on issue due date
-merge_request: 17985
-author: Stuart Nelson
-type: added
diff --git a/changelogs/unreleased/17516-nested-restore-changelog.yml b/changelogs/unreleased/17516-nested-restore-changelog.yml
deleted file mode 100644
index 89753f45457..00000000000
--- a/changelogs/unreleased/17516-nested-restore-changelog.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable restore rake task to handle nested storage directories
-merge_request: 17516
-author: Balasankar C
-type: fixed
diff --git a/changelogs/unreleased/17939-osw-patch-support-gfm.yml b/changelogs/unreleased/17939-osw-patch-support-gfm.yml
deleted file mode 100644
index 576581e25d6..00000000000
--- a/changelogs/unreleased/17939-osw-patch-support-gfm.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for patch link extension for commit links on GitLab Flavored Markdown
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
new file mode 100644
index 00000000000..9287243a7e3
--- /dev/null
+++ b/changelogs/unreleased/18524-fix-double-brackets-in-wiki-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix double-brackets being linkified in wiki markdown
+merge_request: 18524
+author: brewingcode
+type: fixed
diff --git a/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
new file mode 100644
index 00000000000..a97e8a2b5cc
--- /dev/null
+++ b/changelogs/unreleased/19861-expand-api-render-an-arbitrary-markdown-document.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint to render markdown text
+merge_request: 18926
+author: "@blackst0ne"
+type: added
diff --git a/changelogs/unreleased/20394-protected-branches-wildcard.yml b/changelogs/unreleased/20394-protected-branches-wildcard.yml
deleted file mode 100644
index 3fa8ee4f69f..00000000000
--- a/changelogs/unreleased/20394-protected-branches-wildcard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include matching branches and tags in protected branches / tags count
-merge_request:
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/21677-run-pipeline-word.yml b/changelogs/unreleased/21677-run-pipeline-word.yml
deleted file mode 100644
index 9cc280244e4..00000000000
--- a/changelogs/unreleased/21677-run-pipeline-word.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves wording in new pipeline page
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/22647-width-contributors-graphs.yml b/changelogs/unreleased/22647-width-contributors-graphs.yml
new file mode 100644
index 00000000000..87be3a25d8a
--- /dev/null
+++ b/changelogs/unreleased/22647-width-contributors-graphs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix width of contributors graphs
+merge_request: 18639
+author: Paul Vorbach
+type: fixed
diff --git a/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
new file mode 100644
index 00000000000..2b4727c5f03
--- /dev/null
+++ b/changelogs/unreleased/22846-notifications-broken-during-email-address-change-before-email-confirmed.yml
@@ -0,0 +1,6 @@
+---
+title: Fix an issue where the notification email address would be set to an unconfirmed
+ email address
+merge_request: 18474
+author:
+type: fixed
diff --git a/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml b/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml
deleted file mode 100644
index a62137ea2c9..00000000000
--- a/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Send notification emails when push to a merge request
-merge_request: 7610
-author: YarNayar
-type: feature
diff --git a/changelogs/unreleased/23465-print-markdown.yml b/changelogs/unreleased/23465-print-markdown.yml
new file mode 100644
index 00000000000..ba950667acc
--- /dev/null
+++ b/changelogs/unreleased/23465-print-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix print styles for markdown pages
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml b/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml
deleted file mode 100644
index d96f7e54c8d..00000000000
--- a/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds cancel btn to new pages domain page
-merge_request: 18026
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml b/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml
deleted file mode 100644
index f2d5b503661..00000000000
--- a/changelogs/unreleased/30739-fix-joined-information-on-project-members-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `joined` information on project members page
-merge_request: 18290
-author: Fabian Schneider
-type: fixed
diff --git a/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml b/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
deleted file mode 100644
index bc1955bc66f..00000000000
--- a/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Atomic generation of internal ids for issues.
-merge_request: 17580
-author:
-type: other
diff --git a/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml b/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml
deleted file mode 100644
index 5546d26d0fb..00000000000
--- a/changelogs/unreleased/31591-project-deploy-tokens-to-allow-permanent-access.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create Deploy Tokens to allow permanent access to repository and registry
-merge_request: 17894
-author:
-type: added
diff --git a/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml b/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml
deleted file mode 100644
index c73be5a901e..00000000000
--- a/changelogs/unreleased/32617-fix-template-selector-menu-visibility.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix template selector menu visibility when toggling preview mode in file edit
- view
-merge_request: 18118
-author: Fabian Schneider
-type: fixed
diff --git a/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml b/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml
deleted file mode 100644
index 0382ede4565..00000000000
--- a/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Drop JSON response in Project Milestone along with avoiding error
-merge_request: 17977
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml b/changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml
deleted file mode 100644
index c4b5f59b724..00000000000
--- a/changelogs/unreleased/34604-fix-generated-url-for-external-repository.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix generated URL when listing repoitories for import
-merge_request: 17692
-author:
-type: fixed
diff --git a/changelogs/unreleased/35475-lazy-diff.yml b/changelogs/unreleased/35475-lazy-diff.yml
deleted file mode 100644
index bafa66ebe39..00000000000
--- a/changelogs/unreleased/35475-lazy-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: lazy load diffs on merge request discussions
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
deleted file mode 100644
index cec06bf2dfe..00000000000
--- a/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed bug in dropdown selector when selecting the same selection again
-merge_request: 14631
-author: bitsapien
-type: fixed
diff --git a/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml b/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml
deleted file mode 100644
index 30a8dc63983..00000000000
--- a/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Apply NestingDepth (level 5) (framework/dropdowns.scss)
-merge_request: 17820
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
new file mode 100644
index 00000000000..9f07fcdfa0b
--- /dev/null
+++ b/changelogs/unreleased/39584-nesting-depth-5-pages-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Apply NestingDepth (level 5) (pages/pipelines.scss)
+merge_request: 18830
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/39710-search-placeholder-cut-off.yml b/changelogs/unreleased/39710-search-placeholder-cut-off.yml
new file mode 100644
index 00000000000..59290768c6a
--- /dev/null
+++ b/changelogs/unreleased/39710-search-placeholder-cut-off.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fixes: Runners search input placeholder is cut off'
+merge_request: 19015
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/39880-merge-method-api.yml b/changelogs/unreleased/39880-merge-method-api.yml
deleted file mode 100644
index dd44a752c4f..00000000000
--- a/changelogs/unreleased/39880-merge-method-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: Add parameter merge_method to projects'
-merge_request: 18031
-author: Jan Beckmann
-type: added
diff --git a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml b/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
deleted file mode 100644
index e47577f9058..00000000000
--- a/changelogs/unreleased/40402-time-estimate-system-notes-can-be-confusing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a comma to the time estimate system notes
-merge_request: 18326
-author:
-type: changed
diff --git a/changelogs/unreleased/40487-axios-pipelines.yml b/changelogs/unreleased/40487-axios-pipelines.yml
deleted file mode 100644
index 437d5e87e1a..00000000000
--- a/changelogs/unreleased/40487-axios-pipelines.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-title: Replace vue resource with axios in pipelines table
-merge_request:
-author:
-type: other \ No newline at end of file
diff --git a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
new file mode 100644
index 00000000000..e3ebeb5eb61
--- /dev/null
+++ b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml
@@ -0,0 +1,5 @@
+---
+title: Moves MR widget external link icon to the right
+merge_request: 18828
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/40781-os-to-ce.yml b/changelogs/unreleased/40781-os-to-ce.yml
deleted file mode 100644
index 4a364292c60..00000000000
--- a/changelogs/unreleased/40781-os-to-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add object storage support for LFS objects, CI artifacts, and uploads.
-merge_request: 17358
-author:
-type: added
diff --git a/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
new file mode 100644
index 00000000000..58686639594
--- /dev/null
+++ b/changelogs/unreleased/40855_remove_authentication_in_readonly_issue_api.yml
@@ -0,0 +1,5 @@
+---
+title: made listing and showing public issue apis available without authentication
+merge_request: 18638
+author: haseebeqx
+type: changed
diff --git a/changelogs/unreleased/41224-pipeline-icons.yml b/changelogs/unreleased/41224-pipeline-icons.yml
deleted file mode 100644
index 3fe05448d1c..00000000000
--- a/changelogs/unreleased/41224-pipeline-icons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase dropdown width in pipeline graph & center action icon
-merge_request: 18089
-author:
-type: fixed
diff --git a/changelogs/unreleased/41436-use-simpler-env-vars-for-auto-devops-replicas.yml b/changelogs/unreleased/41436-use-simpler-env-vars-for-auto-devops-replicas.yml
deleted file mode 100644
index ea007670332..00000000000
--- a/changelogs/unreleased/41436-use-simpler-env-vars-for-auto-devops-replicas.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436'
-merge_request: 18036
-author:
-type: added
diff --git a/changelogs/unreleased/41748-vertical-misalignment-login-box.yml b/changelogs/unreleased/41748-vertical-misalignment-login-box.yml
deleted file mode 100644
index 77a97400323..00000000000
--- a/changelogs/unreleased/41748-vertical-misalignment-login-box.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactor CSS to eliminate vertical misalignment of login nav
-merge_request: 16275
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml b/changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml
deleted file mode 100644
index 36e79ea1ed4..00000000000
--- a/changelogs/unreleased/41758-after-changing-username-url-still-redirects-to-old-route.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added confirmation modal for changing username
-merge_request: 17405
-author:
-type: added
diff --git a/changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml b/changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml
deleted file mode 100644
index 60a649f22c9..00000000000
--- a/changelogs/unreleased/41902-add-api-option-to-overwrite-project-description-on-project-export.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds the option to the project export API to override the project description and display GitLab export description once imported
-merge_request: 17744
-author:
-type: added
diff --git a/changelogs/unreleased/41967_issue_api_closed_by_info.yml b/changelogs/unreleased/41967_issue_api_closed_by_info.yml
deleted file mode 100644
index 436574c3638..00000000000
--- a/changelogs/unreleased/41967_issue_api_closed_by_info.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: adds closed by informations in issue api
-merge_request: 17042
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml b/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml
deleted file mode 100644
index 30481e7af84..00000000000
--- a/changelogs/unreleased/41981-allow-group-owner-to-enable-runners-from-subgroups.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Allow group owner to enable runners from subgroups (#41981)'
-merge_request: 18009
-author:
-type: fixed
diff --git a/changelogs/unreleased/42028-xss-diffs.yml b/changelogs/unreleased/42028-xss-diffs.yml
deleted file mode 100644
index a05f9d3c78d..00000000000
--- a/changelogs/unreleased/42028-xss-diffs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix XSS on diff view stored on filenames
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml b/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml
deleted file mode 100644
index f7758734a6f..00000000000
--- a/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Long instance urls do not overflow anymore during project creation
-merge_request: 17717
-author:
-type: fixed
diff --git a/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml b/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml
deleted file mode 100644
index 77d1ebf69df..00000000000
--- a/changelogs/unreleased/42448-change-commit-row-actions-and-sha-design-for-project-commit-list.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Improved visual styles and consistency for commit hash and possible actions
- across commit lists
-merge_request: 17406
-author:
-type: changed
diff --git a/changelogs/unreleased/42531-open-invite-404.yml b/changelogs/unreleased/42531-open-invite-404.yml
new file mode 100644
index 00000000000..73729f4a929
--- /dev/null
+++ b/changelogs/unreleased/42531-open-invite-404.yml
@@ -0,0 +1,5 @@
+---
+title: Automatically accepts project/group invite by email after user signup
+merge_request: 17634
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml b/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
deleted file mode 100644
index 7452a264bfd..00000000000
--- a/changelogs/unreleased/42543-hide-divergence-graph-on-branches-for-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove ahead/behind graphs on project branches on mobile
-merge_request: 18415
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/42568-pipeline-empty-state.yml b/changelogs/unreleased/42568-pipeline-empty-state.yml
deleted file mode 100644
index d36edcf1b37..00000000000
--- a/changelogs/unreleased/42568-pipeline-empty-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve empty state for canceled job
-merge_request: 17646
-author:
-type: fixed
diff --git a/changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml b/changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml
deleted file mode 100644
index c0a247dc895..00000000000
--- a/changelogs/unreleased/42579-fix-sidebar-dropdown-hover-style.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix hover style of dropdown items in the right sidebar
-merge_request: 17519
-author:
-type: fixed
diff --git a/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml b/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml
deleted file mode 100644
index 0e892a51bc5..00000000000
--- a/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Firefox stealing formatting characters on issue notes
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/42889-avoid-return-inside-block.yml b/changelogs/unreleased/42889-avoid-return-inside-block.yml
deleted file mode 100644
index e3e1341ddcc..00000000000
--- a/changelogs/unreleased/42889-avoid-return-inside-block.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rubocop rule to avoid returning from a block
-merge_request: 18000
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml b/changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml
deleted file mode 100644
index 686258460e0..00000000000
--- a/changelogs/unreleased/43098-controller-projects-issuescontroller-show-executes-more-than-100-sql-queries.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of loading issues with lots of references to merge requests
-merge_request: 17986
-author:
-type: performance
diff --git a/changelogs/unreleased/43215-update-design-for-verifying-domains.yml b/changelogs/unreleased/43215-update-design-for-verifying-domains.yml
deleted file mode 100644
index 8326540f7b2..00000000000
--- a/changelogs/unreleased/43215-update-design-for-verifying-domains.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Polish design for verifying domains
-merge_request: 17767
-author:
-type: changed
diff --git a/changelogs/unreleased/43246-checkfilter.yml b/changelogs/unreleased/43246-checkfilter.yml
deleted file mode 100644
index e6c0e716213..00000000000
--- a/changelogs/unreleased/43246-checkfilter.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Require at least one filter when listing issues or merge requests on dashboard
- page
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml b/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml
deleted file mode 100644
index de1cee6e436..00000000000
--- a/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use specific names for filtered CI variable controller parameters
-merge_request: 17796
-author:
-type: other
diff --git a/changelogs/unreleased/43367-fix-board-long-strings.yml b/changelogs/unreleased/43367-fix-board-long-strings.yml
new file mode 100644
index 00000000000..6228741601e
--- /dev/null
+++ b/changelogs/unreleased/43367-fix-board-long-strings.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issue board bug with long strings in titles
+merge_request: 18924
+author:
+type: fixed
diff --git a/changelogs/unreleased/43404-pipelines-commit.yml b/changelogs/unreleased/43404-pipelines-commit.yml
deleted file mode 100644
index 0b9a4a6451f..00000000000
--- a/changelogs/unreleased/43404-pipelines-commit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Breaks commit not found message in pipelines table
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml b/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml
deleted file mode 100644
index 889fd008bad..00000000000
--- a/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add empty repo check before running AutoDevOps pipeline
-merge_request: 17605
-author:
-type: changed
diff --git a/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml b/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml
deleted file mode 100644
index 039d3de7168..00000000000
--- a/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds support for OmniAuth JWT provider
-merge_request: 17774
-author:
-type: added
diff --git a/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml b/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml
deleted file mode 100644
index f30fea3c4a7..00000000000
--- a/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Limit the number of failed logins when using LDAP for authentication
-merge_request: 43525
-author:
-type: added
diff --git a/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml b/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml
deleted file mode 100644
index 39f92c281ad..00000000000
--- a/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improves the performance of projects list page
-merge_request: 17934
-author:
-type: performance
diff --git a/changelogs/unreleased/43567-replace-gke.yml b/changelogs/unreleased/43567-replace-gke.yml
deleted file mode 100644
index 8ec79fc3d4d..00000000000
--- a/changelogs/unreleased/43567-replace-gke.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace GKE acronym with Google Kubernetes Engine
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/43603-ci-lint-support.yml b/changelogs/unreleased/43603-ci-lint-support.yml
deleted file mode 100644
index 8e4a92c0287..00000000000
--- a/changelogs/unreleased/43603-ci-lint-support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move ci/lint under project's namespace
-merge_request: 17729
-author:
-type: added
diff --git a/changelogs/unreleased/43617-mailsig.yml b/changelogs/unreleased/43617-mailsig.yml
deleted file mode 100644
index 2c7568e32ca..00000000000
--- a/changelogs/unreleased/43617-mailsig.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use RFC 3676 mail signature delimiters
-merge_request: 17979
-author: Enrico Scholz
-type: changed
diff --git a/changelogs/unreleased/43673-operations-tab-mvc.yml b/changelogs/unreleased/43673-operations-tab-mvc.yml
new file mode 100644
index 00000000000..cd580e7a8d6
--- /dev/null
+++ b/changelogs/unreleased/43673-operations-tab-mvc.yml
@@ -0,0 +1,5 @@
+---
+title: Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'
+merge_request: 18941
+author:
+type: changed
diff --git a/changelogs/unreleased/43702-update-label-dropdown-wording.yml b/changelogs/unreleased/43702-update-label-dropdown-wording.yml
deleted file mode 100644
index 97100ec89de..00000000000
--- a/changelogs/unreleased/43702-update-label-dropdown-wording.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update wording to specify create/manage project vs group labels in labels dropdown
-merge_request: 17640
-author:
-type: changed
diff --git a/changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml b/changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml
deleted file mode 100644
index 3aec71d5ac4..00000000000
--- a/changelogs/unreleased/43717-breadcrumb-on-admin-runner-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Set breadcrumb for admin/runners/show
-merge_request: 17431
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/43720-update-fe-webpack-docs.yml b/changelogs/unreleased/43720-update-fe-webpack-docs.yml
deleted file mode 100644
index 9e461eaaec8..00000000000
--- a/changelogs/unreleased/43720-update-fe-webpack-docs.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Update documentation to reflect current minimum required versions of node and
- yarn
-merge_request: 17706
-author:
-type: other
diff --git a/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml b/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml
deleted file mode 100644
index 6283e797930..00000000000
--- a/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Store sha256 checksum of artifact metadata
-merge_request: 18149
-author:
-type: added
diff --git a/changelogs/unreleased/43771-improve-avatar-error-message.yml b/changelogs/unreleased/43771-improve-avatar-error-message.yml
deleted file mode 100644
index 1fae10f4d1f..00000000000
--- a/changelogs/unreleased/43771-improve-avatar-error-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change avatar error message to include allowed file formats
-merge_request: 17747
-author: Fabian Schneider
-type: changed
diff --git a/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml b/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml
deleted file mode 100644
index 19b633daace..00000000000
--- a/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add tooltips to icons in lists of issues and merge requests
-merge_request: 17700
-author:
-type: changed
diff --git a/changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml b/changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml
deleted file mode 100644
index 4c63e69f0bb..00000000000
--- a/changelogs/unreleased/43805-list-gitaly-calls-and-arguments-in-the-performance-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Gitaly call details to performance bar
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml b/changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml
deleted file mode 100644
index 7335d313510..00000000000
--- a/changelogs/unreleased/43806-update-ruby-saml-to-1-7-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0
-merge_request: 17734
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/43933-always-notify-mentions.yml b/changelogs/unreleased/43933-always-notify-mentions.yml
deleted file mode 100644
index 7b494d38541..00000000000
--- a/changelogs/unreleased/43933-always-notify-mentions.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Send @mention notifications even if a user has explicitly unsubscribed from
- item
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/43949-verify-job-artifacts.yml b/changelogs/unreleased/43949-verify-job-artifacts.yml
deleted file mode 100644
index 45e1916ae17..00000000000
--- a/changelogs/unreleased/43949-verify-job-artifacts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implement foreground verification of CI artifacts
-merge_request: 17578
-author:
-type: added
diff --git a/changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml b/changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml
deleted file mode 100644
index b341d5dfa7f..00000000000
--- a/changelogs/unreleased/43976-fix-access-token-clipboard-button-style.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix personal access token clipboard button style
-merge_request: 17978
-author: Fabian Schneider
-type: fixed
diff --git a/changelogs/unreleased/44022-singular-1-diff.yml b/changelogs/unreleased/44022-singular-1-diff.yml
deleted file mode 100644
index f4942925a73..00000000000
--- a/changelogs/unreleased/44022-singular-1-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use singular in the diff stats if only one line has been changed
-merge_request: 17697
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml b/changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml
deleted file mode 100644
index dd5f2f06d6c..00000000000
--- a/changelogs/unreleased/44139-fix-issue-boards-dup-keys.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Use object ID to prevent duplicate keys Vue warning on Issue Boards page during
- development
-merge_request: 17682
-author:
-type: other
diff --git a/changelogs/unreleased/44160-update-foreman-to-0-84-0.yml b/changelogs/unreleased/44160-update-foreman-to-0-84-0.yml
deleted file mode 100644
index 990d188eb78..00000000000
--- a/changelogs/unreleased/44160-update-foreman-to-0-84-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update foreman from 0.78.0 to 0.84.0
-merge_request: 17690
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml b/changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml
deleted file mode 100644
index 8fdca6eec83..00000000000
--- a/changelogs/unreleased/44191-reduce-redis-usage-from-merge-request-diffs-caching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop caching highlighted diffs in Redis unnecessarily
-merge_request: 17746
-author:
-type: performance
diff --git a/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml b/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml
deleted file mode 100644
index 12c73281998..00000000000
--- a/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added i18n support for the prometheus memory widget
-merge_request: 17753
-author:
-type: other
diff --git a/changelogs/unreleased/44224-remove-gl.yml b/changelogs/unreleased/44224-remove-gl.yml
deleted file mode 100644
index 1c792883f09..00000000000
--- a/changelogs/unreleased/44224-remove-gl.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes modal boards store and mixins from global scope
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml b/changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml
deleted file mode 100644
index 265d36b763f..00000000000
--- a/changelogs/unreleased/44235-update-knapsack-to-1-16-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update knapsack to 1.16.0
-merge_request: 17735
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml b/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
deleted file mode 100644
index 934860b95fe..00000000000
--- a/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix viewing diffs on old merge requests
-merge_request: 17805
-author:
-type: fixed
diff --git a/changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml b/changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml
deleted file mode 100644
index b3ae8ca7340..00000000000
--- a/changelogs/unreleased/44269-show-failure-reason-on-upgrade-tooltip-of-jobs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display error message on job's tooltip if this one fails
-merge_request: 17782
-author:
-type: added
diff --git a/changelogs/unreleased/44280-fix-code-search.yml b/changelogs/unreleased/44280-fix-code-search.yml
deleted file mode 100644
index 07f3abb224c..00000000000
--- a/changelogs/unreleased/44280-fix-code-search.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix search results stripping last endline when parsing the results
-merge_request: 17777
-author: Jasper Maes
-type: fixed
diff --git a/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml b/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
deleted file mode 100644
index b5c12d8f40e..00000000000
--- a/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add additional cluster usage metrics to usage ping.
-merge_request: 17922
-author:
-type: changed
diff --git a/changelogs/unreleased/44296-commit-path.yml b/changelogs/unreleased/44296-commit-path.yml
deleted file mode 100644
index b658178f8dc..00000000000
--- a/changelogs/unreleased/44296-commit-path.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Verifiy if pipeline has commit idetails and render information in MR widget
- when branch is deleted
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44319-remove-gray-buttons.yml b/changelogs/unreleased/44319-remove-gray-buttons.yml
new file mode 100644
index 00000000000..9803dde8493
--- /dev/null
+++ b/changelogs/unreleased/44319-remove-gray-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Remove gray button styles
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml b/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml
deleted file mode 100644
index dd8c0b19d5f..00000000000
--- a/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix UI breakdown for Create merge request button
-merge_request: 17821
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/44383-cleanup-framework-header.yml b/changelogs/unreleased/44383-cleanup-framework-header.yml
deleted file mode 100644
index ef9be9f48de..00000000000
--- a/changelogs/unreleased/44383-cleanup-framework-header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clean up selectors in framework/header.scss
-merge_request: 17822
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml b/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
deleted file mode 100644
index 79c470ea4e1..00000000000
--- a/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Unify format for nested non-task lists
-merge_request: 17823
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml b/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml
deleted file mode 100644
index 16712486f0f..00000000000
--- a/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: UX re-design branch items with flexbox
-merge_request: 17832
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml b/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml
deleted file mode 100644
index c21d02d4d87..00000000000
--- a/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update rack-protection to 2.0.1
-merge_request: 17835
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml b/changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml
deleted file mode 100644
index 2e5a0302ee6..00000000000
--- a/changelogs/unreleased/44389-always-allow-http-for-ci-git-operations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow HTTP(s) when git request is made by GitLab CI
-merge_request: 18021
-author:
-type: changed
diff --git a/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml b/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
deleted file mode 100644
index 3bbd5a05b98..00000000000
--- a/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Project creation will now raise an error if a service template is invalid
-merge_request: 18013
-author:
-type: fixed
diff --git a/changelogs/unreleased/44425-use-gitlab_environment.yml b/changelogs/unreleased/44425-use-gitlab_environment.yml
deleted file mode 100644
index a774143d5f5..00000000000
--- a/changelogs/unreleased/44425-use-gitlab_environment.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`
-merge_request: 18154
-author:
-type: fixed
diff --git a/changelogs/unreleased/44508-fix-fork-namespace-images.yml b/changelogs/unreleased/44508-fix-fork-namespace-images.yml
deleted file mode 100644
index 63b4b9a5e56..00000000000
--- a/changelogs/unreleased/44508-fix-fork-namespace-images.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug rendering group icons when forking
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml b/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml
deleted file mode 100644
index ff734fe0c05..00000000000
--- a/changelogs/unreleased/44541-fix-file-tree-commit-status-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix pipeline status in branch/tag tree page
-merge_request: 17995
-author:
-type: fixed
diff --git a/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml b/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml
new file mode 100644
index 00000000000..21e7c795815
--- /dev/null
+++ b/changelogs/unreleased/44579-ide-add-pipeline-to-status-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Add pipeline status to the status bar of the Web IDE
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml b/changelogs/unreleased/44582-clear-pipeline-status-cache.yml
deleted file mode 100644
index 1777f2ffaab..00000000000
--- a/changelogs/unreleased/44582-clear-pipeline-status-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Now `rake cache:clear` will also clear pipeline status cache
-merge_request: 18257
-author:
-type: fixed
diff --git a/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml b/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
deleted file mode 100644
index 4f21aadd86b..00000000000
--- a/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reuse root_ref_hash for performance on Branches
-merge_request: 17998
-author: Takuya Noguchi
-type: performance
diff --git a/changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml b/changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml
deleted file mode 100644
index 4166d4fe320..00000000000
--- a/changelogs/unreleased/44665-fix-db-trace-stream-by-raw-access.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `JobsController#raw` endpoint can not read traces in database
-merge_request: 18101
-author:
-type: fixed
diff --git a/changelogs/unreleased/44697-prevue.yml b/changelogs/unreleased/44697-prevue.yml
deleted file mode 100644
index 9fdce5869ae..00000000000
--- a/changelogs/unreleased/44697-prevue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make toggle markdown preview shortcut only toggle selected field
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
deleted file mode 100644
index bdfed89d2ea..00000000000
--- a/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update asciidoctor-plantuml to 0.0.8
-merge_request: 18022
-author: Takuya Noguchi
-type: performance
diff --git a/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml b/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
deleted file mode 100644
index 372f4293964..00000000000
--- a/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed gitlab:uploads:migrate task ignoring some uploads.
-merge_request: 18082
-author:
-type: fixed
diff --git a/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml b/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
deleted file mode 100644
index 6094fcd0b3e..00000000000
--- a/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed gitlab:uploads:migrate task failing for Groups' avatar.
-merge_request: 18088
-author:
-type: fixed
diff --git a/changelogs/unreleased/44799-api-naming-issue-scope.yml b/changelogs/unreleased/44799-api-naming-issue-scope.yml
new file mode 100644
index 00000000000..75c6ea4cd0d
--- /dev/null
+++ b/changelogs/unreleased/44799-api-naming-issue-scope.yml
@@ -0,0 +1,5 @@
+---
+title: Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me
+merge_request: 44799
+author:
+type: deprecated
diff --git a/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml b/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
deleted file mode 100644
index f5710cf4f7f..00000000000
--- a/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update brakeman 3.6.1 to 4.2.1
-merge_request: 18122
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44902-remove-rake-test-ci.yml b/changelogs/unreleased/44902-remove-rake-test-ci.yml
deleted file mode 100644
index 459de1c2ca3..00000000000
--- a/changelogs/unreleased/44902-remove-rake-test-ci.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove test_ci rake task
-merge_request: 18139
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml b/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml
deleted file mode 100644
index 64a17990ebc..00000000000
--- a/changelogs/unreleased/44981-http-io-trace-with-multi-byte-char.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix `Trace::HttpIO` can not render multi-byte chars
-merge_request: 18417
-author:
-type: fixed
diff --git a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml b/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
deleted file mode 100644
index 4af2af2a561..00000000000
--- a/changelogs/unreleased/44985-fix-protected-branch-delete-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix confirmation modal for deleting a protected branch
-merge_request: 18176
-author: Paul Bonaud @PaulRbR
-type: fixed
diff --git a/changelogs/unreleased/45065-users-projects-json-sort.yml b/changelogs/unreleased/45065-users-projects-json-sort.yml
new file mode 100644
index 00000000000..89a1d7eb36f
--- /dev/null
+++ b/changelogs/unreleased/45065-users-projects-json-sort.yml
@@ -0,0 +1,5 @@
+---
+title: Order UsersController#projects.json by updated_at
+merge_request: 18227
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/45159-fix-illustration.yml b/changelogs/unreleased/45159-fix-illustration.yml
deleted file mode 100644
index 3b9cb45b916..00000000000
--- a/changelogs/unreleased/45159-fix-illustration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds illustration for when job log was erased
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/45190-create-notes-diff-files.yml b/changelogs/unreleased/45190-create-notes-diff-files.yml
new file mode 100644
index 00000000000..efe322b682d
--- /dev/null
+++ b/changelogs/unreleased/45190-create-notes-diff-files.yml
@@ -0,0 +1,5 @@
+---
+title: Persist truncated note diffs on a new table
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/45271-collpased-diff-loading.yml b/changelogs/unreleased/45271-collpased-diff-loading.yml
deleted file mode 100644
index fdd13a82a4c..00000000000
--- a/changelogs/unreleased/45271-collpased-diff-loading.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes unresolved discussions rendering the error state instead of the diff
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/45287-align-icons.yml b/changelogs/unreleased/45287-align-icons.yml
deleted file mode 100644
index 0a1cccf9ca6..00000000000
--- a/changelogs/unreleased/45287-align-icons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Align action icons in pipeline graph
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml b/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml
deleted file mode 100644
index 963ec893963..00000000000
--- a/changelogs/unreleased/45363-optional-params-on-api-endpoint-produce-invalid-pagination-header-links.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: '[API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors`
- when no value is passed for `order_by` or `sort`'
-merge_request: 18393
-author:
-type: fixed
diff --git a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml b/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml
deleted file mode 100644
index 3370ec3feba..00000000000
--- a/changelogs/unreleased/45397-update-faraday_middleware-to-0-12-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update faraday_middlewar to 0.12.2
-merge_request: 18397
-author: Takuya Noguchi
-type: security
diff --git a/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml b/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml
new file mode 100644
index 00000000000..af2cb65445c
--- /dev/null
+++ b/changelogs/unreleased/45404-remove-gemnasium-badge-from-project-s-readme-md.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Gemnasium badge from project README.md
+merge_request: 19136
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml b/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
deleted file mode 100644
index 0f1d111ca58..00000000000
--- a/changelogs/unreleased/45436-markdown-is-not-rendering-error-loading-viewer-undefined-method-html_escape.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix undefined `html_escape` method during markdown rendering
-merge_request: 18418
-author:
-type: fixed
diff --git a/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml b/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
new file mode 100644
index 00000000000..0694206d4fb
--- /dev/null
+++ b/changelogs/unreleased/45442-updates-updated-at-to-issue-on-time-spent.yml
@@ -0,0 +1,5 @@
+---
+title: Updates updated_at on issuable when setting time spent
+merge_request: 18757
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml b/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml
new file mode 100644
index 00000000000..31b4c29e03d
--- /dev/null
+++ b/changelogs/unreleased/45584-add-nip-io-domain-suggestion-in-auto-devops.yml
@@ -0,0 +1,5 @@
+---
+title: Display help text below auto devops domain with nip.io domain name (#45561)
+merge_request: 18496
+author:
+type: added
diff --git a/changelogs/unreleased/45715-remove-modal-retry.yml b/changelogs/unreleased/45715-remove-modal-retry.yml
new file mode 100644
index 00000000000..04f2ff5142e
--- /dev/null
+++ b/changelogs/unreleased/45715-remove-modal-retry.yml
@@ -0,0 +1,5 @@
+---
+title: Remove modalbox confirmation when retrying a pipeline
+merge_request: 18879
+author:
+type: changed
diff --git a/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
new file mode 100644
index 00000000000..7c495cf4dc0
--- /dev/null
+++ b/changelogs/unreleased/45827-expose_readme_url_in_project_api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose readme url in Project API
+merge_request: 18960
+author: Imre Farkas
+type: changed
diff --git a/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml b/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml
new file mode 100644
index 00000000000..c3955c9d8b3
--- /dev/null
+++ b/changelogs/unreleased/45850-close-mr-checkout-modal-on-escape.yml
@@ -0,0 +1,5 @@
+---
+title: Closes MR check out branch modal with escape
+merge_request: Jacopo Beschi @jacopo-beschi
+author: 19050
+type: added
diff --git a/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
new file mode 100644
index 00000000000..b9e70bc5679
--- /dev/null
+++ b/changelogs/unreleased/45934-ide-firefox-scroll-md-preview.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unscrollable Markdown preview of WebIDE on Firefox
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/46010-add-index-to-runner-type.yml b/changelogs/unreleased/46010-add-index-to-runner-type.yml
new file mode 100644
index 00000000000..fb8340e37b2
--- /dev/null
+++ b/changelogs/unreleased/46010-add-index-to-runner-type.yml
@@ -0,0 +1,5 @@
+---
+title: Add index on runner_type for ci_runners
+merge_request: 18897
+author:
+type: performance
diff --git a/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
new file mode 100644
index 00000000000..07f67251b24
--- /dev/null
+++ b/changelogs/unreleased/46082-runner-contacted_at-is-not-always-a-time-type.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Runner contacted at tooltip cache.
+merge_request: 18810
+author:
+type: fixed
diff --git a/changelogs/unreleased/46193-fix-big-estimate.yml b/changelogs/unreleased/46193-fix-big-estimate.yml
new file mode 100644
index 00000000000..d0da0c10033
--- /dev/null
+++ b/changelogs/unreleased/46193-fix-big-estimate.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes 500 error on /estimate BIG_VALUE
+merge_request: 18964
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/46354-deprecate_gemnasium_service.yml b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
new file mode 100644
index 00000000000..c5ead45d883
--- /dev/null
+++ b/changelogs/unreleased/46354-deprecate_gemnasium_service.yml
@@ -0,0 +1,5 @@
+---
+title: Deprecate Gemnasium project service
+merge_request: 18954
+author:
+type: deprecated
diff --git a/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
new file mode 100644
index 00000000000..e4255f11ecf
--- /dev/null
+++ b/changelogs/unreleased/46361-does-not-log-failed-sign-in-attempts-when-the-database-is-in-read-only-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Does not log failed sign-in attempts when the database is in read-only mode
+merge_request: 18957
+author:
+type: fixed
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
new file mode 100644
index 00000000000..609968f3230
--- /dev/null
+++ b/changelogs/unreleased/46427-add-keyboard-shortcut-environments.yml
@@ -0,0 +1,5 @@
+---
+title: Adds keyboard shortcut `g e` for Environments on Project pages
+merge_request: 19002
+author:
+type: added
diff --git a/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
new file mode 100644
index 00000000000..48e51b2615e
--- /dev/null
+++ b/changelogs/unreleased/46427-add-keyboard-shortcut-kubernetes.yml
@@ -0,0 +1,5 @@
+---
+title: Adds keyboard shortcut `g k` for Kubernetes on Project pages
+merge_request: 19002
+author:
+type: added
diff --git a/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
new file mode 100644
index 00000000000..9a7cf0d6944
--- /dev/null
+++ b/changelogs/unreleased/46427-change-keyboard-shortcut-of-activity-feed.yml
@@ -0,0 +1,5 @@
+---
+title: Changes keyboard shortcut of Activity feed to `g v`
+merge_request: 19002
+author:
+type: changed
diff --git a/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
new file mode 100644
index 00000000000..f416e35030e
--- /dev/null
+++ b/changelogs/unreleased/46427-remove-outdated-todos-shortcut.yml
@@ -0,0 +1,5 @@
+---
+title: Removes outdated `g t` shortcut for TODO in favor of `Shift+T`
+merge_request: 19002
+author:
+type: removed
diff --git a/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml b/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml
new file mode 100644
index 00000000000..1d0b11cfd2a
--- /dev/null
+++ b/changelogs/unreleased/46600-fix-gitlab-revision-when-not-in-git-repo.yml
@@ -0,0 +1,6 @@
+---
+title: Replace Gitlab::REVISION with Gitlab.revision and handle installations without
+ a .git directory
+merge_request: 19125
+author:
+type: fixed
diff --git a/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml b/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml
new file mode 100644
index 00000000000..3707ad74b8f
--- /dev/null
+++ b/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update redis-namespace to 1.6.0
+merge_request: 19166
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/8088_embedded_snippets_support.yml b/changelogs/unreleased/8088_embedded_snippets_support.yml
deleted file mode 100644
index 7bd77a69dbd..00000000000
--- a/changelogs/unreleased/8088_embedded_snippets_support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds Embedded Snippets Support
-merge_request: 15695
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/Link_to_project_labels_page.yml b/changelogs/unreleased/Link_to_project_labels_page.yml
deleted file mode 100644
index 7bdeec423fc..00000000000
--- a/changelogs/unreleased/Link_to_project_labels_page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Always display Labels section in issuable sidebar, even when the project has no labels
-merge_request: 18081
-author: Branka Martinovic
-type: fixed
diff --git a/changelogs/unreleased/ab-37125-assigned-issues-query.yml b/changelogs/unreleased/ab-37125-assigned-issues-query.yml
deleted file mode 100644
index 5d4aad08764..00000000000
--- a/changelogs/unreleased/ab-37125-assigned-issues-query.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reduce complexity of issuable finder query.
-merge_request: 18219
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml b/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml
deleted file mode 100644
index 55069b1f2d2..00000000000
--- a/changelogs/unreleased/ab-37462-cache-personal-projects-count.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache personal projects count.
-merge_request: 18197
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml b/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml
deleted file mode 100644
index 502c1176d2d..00000000000
--- a/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove N+1 query for Noteable association.
-merge_request: 17956
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-43706-composite-primary-keys.yml b/changelogs/unreleased/ab-43706-composite-primary-keys.yml
new file mode 100644
index 00000000000..b17050a64c8
--- /dev/null
+++ b/changelogs/unreleased/ab-43706-composite-primary-keys.yml
@@ -0,0 +1,5 @@
+---
+title: Add NOT NULL constraints to project_authorizations.
+merge_request: 18980
+author:
+type: other
diff --git a/changelogs/unreleased/ab-44467-remove-index.yml b/changelogs/unreleased/ab-44467-remove-index.yml
deleted file mode 100644
index fb772ce85d5..00000000000
--- a/changelogs/unreleased/ab-44467-remove-index.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove unused index from events table.
-merge_request: 18014
-author:
-type: other
diff --git a/changelogs/unreleased/ab-45247-project-lookups-validation.yml b/changelogs/unreleased/ab-45247-project-lookups-validation.yml
deleted file mode 100644
index cd5ebdebc58..00000000000
--- a/changelogs/unreleased/ab-45247-project-lookups-validation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Validate project path prior to hitting the database.
-merge_request: 18322
-author:
-type: performance
diff --git a/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
new file mode 100644
index 00000000000..88ef62ebc0e
--- /dev/null
+++ b/changelogs/unreleased/ab-46530-mediumtext-for-gpg-keys.yml
@@ -0,0 +1,5 @@
+---
+title: Increase text limit for GPG keys (mysql only).
+merge_request: 19069
+author:
+type: other
diff --git a/changelogs/unreleased/ac-fix-use_file-race.yml b/changelogs/unreleased/ac-fix-use_file-race.yml
deleted file mode 100644
index f1315d5d50e..00000000000
--- a/changelogs/unreleased/ac-fix-use_file-race.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix data race between ObjectStorage background_upload and Pages publishing
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
deleted file mode 100644
index 4db7f76e0af..00000000000
--- a/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Port direct upload of LFS artifacts from EE
-merge_request: 17752
-author:
-type: added
diff --git a/changelogs/unreleased/ac-pages-port.yml b/changelogs/unreleased/ac-pages-port.yml
deleted file mode 100644
index 4f7257b4798..00000000000
--- a/changelogs/unreleased/ac-pages-port.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add missing port to artifact links
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml b/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml
deleted file mode 100644
index 3b057373e7d..00000000000
--- a/changelogs/unreleased/adamco-gitlab-ce-move-issue-command.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add slash command for moving issues
-merge_request:
-author: Adam Pahlevi
-type: added
diff --git a/changelogs/unreleased/add-canary-favicon.yml b/changelogs/unreleased/add-canary-favicon.yml
deleted file mode 100644
index 1af6572588d..00000000000
--- a/changelogs/unreleased/add-canary-favicon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add yellow favicon when `CANARY=true` to differientate canary environment
-merge_request: 12477
-author:
-type: changed
diff --git a/changelogs/unreleased/add-cpu-mem-totals.yml b/changelogs/unreleased/add-cpu-mem-totals.yml
deleted file mode 100644
index bc8babab731..00000000000
--- a/changelogs/unreleased/add-cpu-mem-totals.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Total CPU/Memory consumption metrics for Kubernetes
-merge_request: 17731
-author:
-type: added
diff --git a/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml b/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
deleted file mode 100644
index 015bee99170..00000000000
--- a/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update dashboard milestones breadcrumb link
-merge_request: 17933
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/add-per-runner-job-timeout.yml b/changelogs/unreleased/add-per-runner-job-timeout.yml
deleted file mode 100644
index 336b4d15ddf..00000000000
--- a/changelogs/unreleased/add-per-runner-job-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add per-runner configured job timeout
-merge_request: 17221
-author:
-type: added
diff --git a/changelogs/unreleased/add-query-counts-to-profiler-output.yml b/changelogs/unreleased/add-query-counts-to-profiler-output.yml
deleted file mode 100644
index 8a90b1cbeb0..00000000000
--- a/changelogs/unreleased/add-query-counts-to-profiler-output.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add query counts to profiler output
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/ajax-requests-in-performance-bar.yml b/changelogs/unreleased/ajax-requests-in-performance-bar.yml
deleted file mode 100644
index 88cc3678c2b..00000000000
--- a/changelogs/unreleased/ajax-requests-in-performance-bar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow viewing timings for AJAX requests in the performance bar
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml b/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml
deleted file mode 100644
index b49c48e0fe1..00000000000
--- a/changelogs/unreleased/ash-mckenzie-include-sha-with-version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: git SHA is now displayed alongside the GitLab version on the Admin Dashboard
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml b/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
deleted file mode 100644
index 9885c8853cc..00000000000
--- a/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump html-pipeline to 2.7.1
-merge_request: 18132
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml b/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml
deleted file mode 100644
index a9c6fcbf428..00000000000
--- a/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump `state_machines-activerecord` to 0.5.1
-merge_request: 17924
-author: blackst0ne
-type: other
diff --git a/changelogs/unreleased/blackst0ne-remove-spinach.yml b/changelogs/unreleased/blackst0ne-remove-spinach.yml
new file mode 100644
index 00000000000..104da257bad
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-remove-spinach.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Spinach
+merge_request: 18869
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml
deleted file mode 100644
index bcfba4ae70d..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-branches-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Replace the `project/commits/branches.feature` spinach test with an rspec analog"
-merge_request: 18302
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml
deleted file mode 100644
index e7077f27555..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-commits-comments-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the `project/commits/comments.feature` spinach test with an rspec analog
-merge_request: 18356
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
new file mode 100644
index 00000000000..7014de4ece7
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-deploy-keys-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/deploy_keys.feature` spinach test with an rspec analog'
+merge_request: 18796
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
new file mode 100644
index 00000000000..7802391ec64
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-ff-merge-requests-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/ff_merge_requests.feature` spinach test with an rspec analog'
+merge_request: 18800
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
new file mode 100644
index 00000000000..2ac43490c26
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-forked-merge-requests-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog'
+merge_request: 18867
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml
deleted file mode 100644
index 7defdc0a28f..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the spinach test with an rspec analog
-merge_request: 17950
-author: blackst0ne
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml
deleted file mode 100644
index 4e1bb15f150..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the `project/issues/labels.feature` spinach test with an rspec analog
-merge_request: 18126
-author: blackst0ne
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml
deleted file mode 100644
index 0dcac0a80eb..00000000000
--- a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-milestones-feature.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace the `project/issues/milestones.feature` spinach test with an rspec analog
-merge_request: 18300
-author: "@blackst0ne"
-type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
new file mode 100644
index 00000000000..968a937ca5a
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-references-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/issues/references.feature` spinach test with an rspec analog'
+merge_request: 18769
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
new file mode 100644
index 00000000000..c0ba984bfdc
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml
@@ -0,0 +1,5 @@
+---
+title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog'
+merge_request: 18794
+author: '@blackst0ne'
+type: other
diff --git a/changelogs/unreleased/bvl-add-username-to-terms-message.yml b/changelogs/unreleased/bvl-add-username-to-terms-message.yml
new file mode 100644
index 00000000000..b95d87e9265
--- /dev/null
+++ b/changelogs/unreleased/bvl-add-username-to-terms-message.yml
@@ -0,0 +1,5 @@
+---
+title: Add username to terms message in git and API calls
+merge_request: 19126
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-export-import-lfs.yml b/changelogs/unreleased/bvl-export-import-lfs.yml
deleted file mode 100644
index dd1f499c3a3..00000000000
--- a/changelogs/unreleased/bvl-export-import-lfs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support LFS objects when importing/exporting GitLab project archives
-merge_request: 18115
-author:
-type: added
diff --git a/changelogs/unreleased/bvl-import-zip-multiple-assignees.yml b/changelogs/unreleased/bvl-import-zip-multiple-assignees.yml
deleted file mode 100644
index 86bd5faf8ed..00000000000
--- a/changelogs/unreleased/bvl-import-zip-multiple-assignees.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix importing multiple assignees from GitLab export
-merge_request: 17718
-author:
-type: fixed
diff --git a/changelogs/unreleased/bvl-no-permanent-redirect.yml b/changelogs/unreleased/bvl-no-permanent-redirect.yml
deleted file mode 100644
index c34a3789b58..00000000000
--- a/changelogs/unreleased/bvl-no-permanent-redirect.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't create permanent redirect routes
-merge_request: 17521
-author:
-type: changed
diff --git a/changelogs/unreleased/bvl-override-import-params.yml b/changelogs/unreleased/bvl-override-import-params.yml
deleted file mode 100644
index 18cfef873df..00000000000
--- a/changelogs/unreleased/bvl-override-import-params.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow overriding params on project import through API
-merge_request: 18086
-author:
-type: added
diff --git a/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
new file mode 100644
index 00000000000..a0d787e570e
--- /dev/null
+++ b/changelogs/unreleased/ccr-incoming-email-regex-anchor.yml
@@ -0,0 +1,3 @@
+title: Add anchor for incoming email regex
+merge_request: !18917
+type: added
diff --git a/changelogs/unreleased/ci-pipeline-commit-lookup.yml b/changelogs/unreleased/ci-pipeline-commit-lookup.yml
deleted file mode 100644
index b2a1e4c2163..00000000000
--- a/changelogs/unreleased/ci-pipeline-commit-lookup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use porcelain commit lookup method on CI::CreatePipelineService
-merge_request: 17911
-author:
-type: fixed
diff --git a/changelogs/unreleased/collapsed-contextual-nav-update.yml b/changelogs/unreleased/collapsed-contextual-nav-update.yml
new file mode 100644
index 00000000000..31a32a9e1e9
--- /dev/null
+++ b/changelogs/unreleased/collapsed-contextual-nav-update.yml
@@ -0,0 +1,6 @@
+---
+title: Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project
+ level
+merge_request: 18996
+author: Constance Okoghenun
+type: fixed
diff --git a/changelogs/unreleased/commit-branch-tag-icon-update.yml b/changelogs/unreleased/commit-branch-tag-icon-update.yml
new file mode 100644
index 00000000000..136b7cbf0f4
--- /dev/null
+++ b/changelogs/unreleased/commit-branch-tag-icon-update.yml
@@ -0,0 +1,5 @@
+---
+title: Updated icons for branch and tag names in commit details
+merge_request: 18953
+author: Constance Okoghenun
+type: changed
diff --git a/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
new file mode 100644
index 00000000000..f32c70cf884
--- /dev/null
+++ b/changelogs/unreleased/create-live-trace-only-if-job-is-complete.yml
@@ -0,0 +1,5 @@
+---
+title: Forbid to patch traces for finished jobs
+merge_request: 18969
+author:
+type: fixed
diff --git a/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml b/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml
deleted file mode 100644
index de09f87a7c9..00000000000
--- a/changelogs/unreleased/da-gitaly-calculate-repository-checksum.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Repository checksum calculation is handled by Gitaly when feature is enabled
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
deleted file mode 100644
index 92a03070d78..00000000000
--- a/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users
-merge_request: 17860
-author: Elias Werberich
-type: added
diff --git a/changelogs/unreleased/deploy-tokens-container-registry-specs.yml b/changelogs/unreleased/deploy-tokens-container-registry-specs.yml
deleted file mode 100644
index d86f955c966..00000000000
--- a/changelogs/unreleased/deploy-tokens-container-registry-specs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Verify that deploy token has valid access when pulling container registry image
-merge_request: 18260
-author:
-type: fixed
diff --git a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml
deleted file mode 100644
index 3e1ac7b795d..00000000000
--- a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add deprecation message to dynamic milestone pages
-merge_request: 17505
-author:
-type: added
diff --git a/changelogs/unreleased/direct-upload-of-uploads.yml b/changelogs/unreleased/direct-upload-of-uploads.yml
deleted file mode 100644
index 7900fa5f58d..00000000000
--- a/changelogs/unreleased/direct-upload-of-uploads.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow to store uploads by default on Object Storage
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/dm-deploy-keys-default-user.yml b/changelogs/unreleased/dm-deploy-keys-default-user.yml
deleted file mode 100644
index b82d67d028c..00000000000
--- a/changelogs/unreleased/dm-deploy-keys-default-user.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure hooks run when a deploy key without a user pushes
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-flatten-tree-plus-chars.yml b/changelogs/unreleased/dm-flatten-tree-plus-chars.yml
deleted file mode 100644
index 23f1b30d8fa..00000000000
--- a/changelogs/unreleased/dm-flatten-tree-plus-chars.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix links to subdirectories of a directory with a plus character in its path
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-internal-user-namespace.yml b/changelogs/unreleased/dm-internal-user-namespace.yml
deleted file mode 100644
index 8517c116795..00000000000
--- a/changelogs/unreleased/dm-internal-user-namespace.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure internal users (ghost, support bot) get assigned a namespace
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/docs-42067-document-runner-registration-api.yml b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
new file mode 100644
index 00000000000..6b507174044
--- /dev/null
+++ b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expand documentation for Runners API
+merge_request: 16484
+author:
+type: other
diff --git a/changelogs/unreleased/docs-for-failure-reason-tooltip.yml b/changelogs/unreleased/docs-for-failure-reason-tooltip.yml
deleted file mode 100644
index ef37654b189..00000000000
--- a/changelogs/unreleased/docs-for-failure-reason-tooltip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add documentation for Pipelines failure reasons
-merge_request: 18352
-author:
-type: other
diff --git a/changelogs/unreleased/dz-add-2fa-filter.yml b/changelogs/unreleased/dz-add-2fa-filter.yml
new file mode 100644
index 00000000000..82d501d6604
--- /dev/null
+++ b/changelogs/unreleased/dz-add-2fa-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Add 2FA filter to the group members page
+merge_request: 18483
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-improve-app-settings-2.yml b/changelogs/unreleased/dz-improve-app-settings-2.yml
deleted file mode 100644
index ebe571decb8..00000000000
--- a/changelogs/unreleased/dz-improve-app-settings-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Redesign application settings to match project settings
-merge_request: 18019
-author:
-type: changed
diff --git a/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml b/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
deleted file mode 100644
index eea9da4c579..00000000000
--- a/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Escape Markdown characters properly when using autocomplete
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/expose-commits-mr-api.yml b/changelogs/unreleased/expose-commits-mr-api.yml
deleted file mode 100644
index 77ea2f27431..00000000000
--- a/changelogs/unreleased/expose-commits-mr-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow merge requests related to a commit to be found via API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml b/changelogs/unreleased/feature-add-language-in-repository-to-api.yml
deleted file mode 100644
index bd9bd377212..00000000000
--- a/changelogs/unreleased/feature-add-language-in-repository-to-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: add languages of project GET /projects/:id/languages'
-merge_request: 17770
-author: Roger Rüttimann
-type: added
diff --git a/changelogs/unreleased/feature-add_target_to_tags.yml b/changelogs/unreleased/feature-add_target_to_tags.yml
deleted file mode 100644
index 75816005e1f..00000000000
--- a/changelogs/unreleased/feature-add_target_to_tags.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose the target commit ID through the tag API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/feature-expose-runner-ip-to-api.yml b/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
new file mode 100644
index 00000000000..e755cf5f2d4
--- /dev/null
+++ b/changelogs/unreleased/feature-expose-runner-ip-to-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose runner ip address to runners API
+merge_request: 18799
+author: Lars Greiss
+type: changed
diff --git a/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
new file mode 100644
index 00000000000..d77c5b42497
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-add-regexp-variables-expression.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for variables expression pattern matching syntax
+merge_request: 18902
+author:
+type: added
diff --git a/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml b/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
deleted file mode 100644
index 84977ce11c8..00000000000
--- a/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for pipeline variables expressions in only/except
-merge_request: 17316
-author:
-type: added
diff --git a/changelogs/unreleased/feature_detect_co_authored_commits.yml b/changelogs/unreleased/feature_detect_co_authored_commits.yml
deleted file mode 100644
index 7b1269ed982..00000000000
--- a/changelogs/unreleased/feature_detect_co_authored_commits.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Detect commit message trailers and link users properly to their accounts
- on Gitlab
-merge_request: 17919
-author: cousine
-type: added
diff --git a/changelogs/unreleased/fix-40798-namespace-forking.yml b/changelogs/unreleased/fix-40798-namespace-forking.yml
deleted file mode 100644
index 095235725f8..00000000000
--- a/changelogs/unreleased/fix-40798-namespace-forking.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix forking to subgroup via API when namespace is given by name
-merge_request: 17815
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/fix-42459---in-branch.yml b/changelogs/unreleased/fix-42459---in-branch.yml
deleted file mode 100644
index 26cc2046206..00000000000
--- a/changelogs/unreleased/fix-42459---in-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix relative uri when "#" is in branch name
-merge_request:
-author: Jan
-type: fixed
diff --git a/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml b/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml
deleted file mode 100644
index e21554f091a..00000000000
--- a/changelogs/unreleased/fix-500-error-when-mr-ref-is-not-yet-fetched.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix 500 error when a merge request from a fork has conflicts and has not yet
- been updated
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-assignee-name-wrap.yml b/changelogs/unreleased/fix-assignee-name-wrap.yml
new file mode 100644
index 00000000000..2407288785f
--- /dev/null
+++ b/changelogs/unreleased/fix-assignee-name-wrap.yml
@@ -0,0 +1,5 @@
+---
+title: Wrapping problem on the issues page has been fixed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-auth0-unsafe-login.yml b/changelogs/unreleased/fix-auth0-unsafe-login.yml
deleted file mode 100644
index 01c6ea69dcc..00000000000
--- a/changelogs/unreleased/fix-auth0-unsafe-login.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix GitLab Auth0 integration signing in the wrong user
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/fix-dashboard-sorting.yml b/changelogs/unreleased/fix-dashboard-sorting.yml
deleted file mode 100644
index 2ba13a93fa9..00000000000
--- a/changelogs/unreleased/fix-dashboard-sorting.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prioritize weight over title when sorting charts
-merge_request: 18233
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-devops-remove-beta.yml b/changelogs/unreleased/fix-devops-remove-beta.yml
new file mode 100644
index 00000000000..326003eb956
--- /dev/null
+++ b/changelogs/unreleased/fix-devops-remove-beta.yml
@@ -0,0 +1,5 @@
+---
+title: Removed "(Beta)" from "Auto DevOps" messages
+merge_request: 18759
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-emoji-popup.yml b/changelogs/unreleased/fix-emoji-popup.yml
deleted file mode 100644
index c81d061a5d7..00000000000
--- a/changelogs/unreleased/fix-emoji-popup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide emoji popup after multiple spaces
-merge_request:
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
new file mode 100644
index 00000000000..92426832f30
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-exclude-persisted-variables-from-environment-name.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude CI_PIPELINE_ID from variables supported in dynamic environment name
+merge_request: 19032
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml b/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml
deleted file mode 100644
index 94010c06a07..00000000000
--- a/changelogs/unreleased/fix-gb-fix-empty-secret-variables.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix a case with secret variables being empty sometimes
-merge_request: 18400
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
new file mode 100644
index 00000000000..c2a788d6ad0
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-not-allow-to-trigger-skipped-manual-actions.yml
@@ -0,0 +1,5 @@
+---
+title: Do not allow to trigger manual actions that were skipped
+merge_request: 18985
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-mattermost-delete-team.yml b/changelogs/unreleased/fix-mattermost-delete-team.yml
deleted file mode 100644
index d14ae023114..00000000000
--- a/changelogs/unreleased/fix-mattermost-delete-team.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed group deletion linked to Mattermost
-merge_request: 16209
-author: Julien Millau
-type: fixed
diff --git a/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml b/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml
deleted file mode 100644
index 837bfed19b7..00000000000
--- a/changelogs/unreleased/fix-n-plus-one-when-getting-notification-settings-for-recipients.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Goldiloader to fix N+1 issues when calculating email recipients
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/fix-projects-no-repository-placeholder.yml b/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
deleted file mode 100644
index 3d11c897020..00000000000
--- a/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update no repository placeholder
-merge_request: 17964
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/fix-reactive-cache-retry-rate.yml b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
new file mode 100644
index 00000000000..044e7fe39c0
--- /dev/null
+++ b/changelogs/unreleased/fix-reactive-cache-retry-rate.yml
@@ -0,0 +1,5 @@
+---
+title: Update commit status from external CI services less aggressively
+merge_request: 18802
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-references-in-group-context.yml b/changelogs/unreleased/fix-references-in-group-context.yml
deleted file mode 100644
index b436c2089ed..00000000000
--- a/changelogs/unreleased/fix-references-in-group-context.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ignore project internal references in group context
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-registry-created-at-tooltip.yml b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
new file mode 100644
index 00000000000..911b3b10fd4
--- /dev/null
+++ b/changelogs/unreleased/fix-registry-created-at-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add missing tooltip to creation date on container registry overview'
+merge_request: 18767
+author: Lars Greiss
+type: fixed
diff --git a/changelogs/unreleased/fix-shorcut-modal.yml b/changelogs/unreleased/fix-shorcut-modal.yml
new file mode 100644
index 00000000000..796a1523a61
--- /dev/null
+++ b/changelogs/unreleased/fix-shorcut-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Fix modal width of shorcuts help page
+merge_request: 18766
+author: Lars Greiss
+type: fixed
diff --git a/changelogs/unreleased/fix-unverified-hover-state.yml b/changelogs/unreleased/fix-unverified-hover-state.yml
new file mode 100644
index 00000000000..003138f9821
--- /dev/null
+++ b/changelogs/unreleased/fix-unverified-hover-state.yml
@@ -0,0 +1,5 @@
+---
+title: Unverified hover state color changed to black
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-wiki-find-file-gitaly.yml b/changelogs/unreleased/fix-wiki-find-file-gitaly.yml
deleted file mode 100644
index 5c536be7ae5..00000000000
--- a/changelogs/unreleased/fix-wiki-find-file-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix finding wiki file when Gitaly is enabled
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml b/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml
deleted file mode 100644
index 7fa6f6a5874..00000000000
--- a/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed some SSRF vulnerabilities in services, hooks and integrations
-merge_request: 2337
-author:
-type: security
diff --git a/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml b/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml
deleted file mode 100644
index be0b83505fb..00000000000
--- a/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add better LDAP connection handling
-merge_request: 18039
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml b/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml
deleted file mode 100644
index 0553cc684ce..00000000000
--- a/changelogs/unreleased/fj-41900-import-endpoint-with-overwrite-support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extend API for importing a project export with overwrite support
-merge_request: 17883
-author:
-type: added
diff --git a/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml b/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
deleted file mode 100644
index a06499d821a..00000000000
--- a/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extend API for exporting a project with direct upload URL
-merge_request: 17686
-author:
-type: added
diff --git a/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
new file mode 100644
index 00000000000..bd4e5a43352
--- /dev/null
+++ b/changelogs/unreleased/fj-46411-fix-badge-api-endpoint-route-with-relative-url.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed badge api endpoint route when relative url is set
+merge_request: 19004
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
new file mode 100644
index 00000000000..16b0ee06898
--- /dev/null
+++ b/changelogs/unreleased/fj-46459-fix-expose-url-when-base-url-set.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed bug where generated api urls didn't add the base url if set
+merge_request: 19003
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml b/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml
deleted file mode 100644
index 53883e8d907..00000000000
--- a/changelogs/unreleased/fj-change-gollum-gems-to-custom-ones.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replacing gollum libraries for gitlab custom libs
-merge_request: 18343
-author:
-type: other
diff --git a/changelogs/unreleased/fl-fix-annoying-actions.yml b/changelogs/unreleased/fl-fix-annoying-actions.yml
deleted file mode 100644
index fe17f9a8978..00000000000
--- a/changelogs/unreleased/fl-fix-annoying-actions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop redirecting the page in pipeline main actions
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fl-pipelines-details-axios.yml b/changelogs/unreleased/fl-pipelines-details-axios.yml
deleted file mode 100644
index 0b72e54cba3..00000000000
--- a/changelogs/unreleased/fl-pipelines-details-axios.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Replace vue resource with axios for pipelines details page
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/ide-file-row-hover-style.yml b/changelogs/unreleased/ide-file-row-hover-style.yml
deleted file mode 100644
index 158379a5aef..00000000000
--- a/changelogs/unreleased/ide-file-row-hover-style.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added hover background color to IDE file list rows
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/ide-folder-button-path.yml b/changelogs/unreleased/ide-folder-button-path.yml
deleted file mode 100644
index 84a122fab75..00000000000
--- a/changelogs/unreleased/ide-folder-button-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed IDE button opening the wrong URL in tree list
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml b/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
new file mode 100644
index 00000000000..9efef2c6839
--- /dev/null
+++ b/changelogs/unreleased/ide-hide-merge-request-if-disabled.yml
@@ -0,0 +1,5 @@
+---
+title: Hide merge request option in IDE when disabled
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/ide-mr-changes-alert-box.yml b/changelogs/unreleased/ide-mr-changes-alert-box.yml
deleted file mode 100644
index fec2719c2b1..00000000000
--- a/changelogs/unreleased/ide-mr-changes-alert-box.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed alert box in IDE when redirecting to new merge request
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ide-project-avatar-identicon.yml b/changelogs/unreleased/ide-project-avatar-identicon.yml
deleted file mode 100644
index 2b8b00018a8..00000000000
--- a/changelogs/unreleased/ide-project-avatar-identicon.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make project avatar in IDE consistent with the rest of GitLab
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/ide-subgroup-fix.yml b/changelogs/unreleased/ide-subgroup-fix.yml
deleted file mode 100644
index 2234c42b4bd..00000000000
--- a/changelogs/unreleased/ide-subgroup-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed IDE not loading for sub groups
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ide-tree-loading-fix.yml b/changelogs/unreleased/ide-tree-loading-fix.yml
deleted file mode 100644
index 2fb43380a48..00000000000
--- a/changelogs/unreleased/ide-tree-loading-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed IDE not showing loading state when tree is loading
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml b/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
deleted file mode 100644
index cee8b8523fd..00000000000
--- a/changelogs/unreleased/improve-jobs-queuing-time-metric.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Partition job_queue_duration_seconds with jobs_running_for_project
-merge_request: 17730
-author:
-type: changed
diff --git a/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml b/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml
deleted file mode 100644
index 6d7d2df4f4a..00000000000
--- a/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase the memory limits used in the unicorn killer
-merge_request: 17948
-author:
-type: other
diff --git a/changelogs/unreleased/issue-25256.yml b/changelogs/unreleased/issue-25256.yml
new file mode 100644
index 00000000000..e981b5f9a8f
--- /dev/null
+++ b/changelogs/unreleased/issue-25256.yml
@@ -0,0 +1,5 @@
+---
+title: Don't trim incoming emails that create new issues
+merge_request:
+author: Cameron Crockett
+type: fixed
diff --git a/changelogs/unreleased/issue_25542.yml b/changelogs/unreleased/issue_25542.yml
deleted file mode 100644
index eba491f7e2a..00000000000
--- a/changelogs/unreleased/issue_25542.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve JIRA event descriptions
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/issue_38418.yml b/changelogs/unreleased/issue_38418.yml
new file mode 100644
index 00000000000..79452b27e4b
--- /dev/null
+++ b/changelogs/unreleased/issue_38418.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issue count on sidebar
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/issue_40915.yml b/changelogs/unreleased/issue_40915.yml
deleted file mode 100644
index 2b6d98e69a6..00000000000
--- a/changelogs/unreleased/issue_40915.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow assigning and filtering issuables by ancestor group labels
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/issue_42443.yml b/changelogs/unreleased/issue_42443.yml
deleted file mode 100644
index 954fbd98a4b..00000000000
--- a/changelogs/unreleased/issue_42443.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include subgroup issues when searching for group issues using the API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/issue_44270.yml b/changelogs/unreleased/issue_44270.yml
deleted file mode 100644
index 6234162be30..00000000000
--- a/changelogs/unreleased/issue_44270.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show issues of subgroups in group-level issue board
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/jej-commit-api-tracks-lfs.yml b/changelogs/unreleased/jej-commit-api-tracks-lfs.yml
deleted file mode 100644
index 8284abf9f28..00000000000
--- a/changelogs/unreleased/jej-commit-api-tracks-lfs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create commit API and Web IDE obey LFS filters
-merge_request: 16718
-author:
-type: fixed
diff --git a/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml b/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml
deleted file mode 100644
index d5219b5d019..00000000000
--- a/changelogs/unreleased/jej-mattermost-notification-confidentiality.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds confidential notes channel for Slack/Mattermost
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/jivl-add-dot-system-notes.yml b/changelogs/unreleased/jivl-add-dot-system-notes.yml
new file mode 100644
index 00000000000..2246bab1464
--- /dev/null
+++ b/changelogs/unreleased/jivl-add-dot-system-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Add dot to separate system notes content
+merge_request: 18864
+author:
+type: changed
diff --git a/changelogs/unreleased/jivl-realtime-update-adding-file.yml b/changelogs/unreleased/jivl-realtime-update-adding-file.yml
deleted file mode 100644
index df1bdb1648d..00000000000
--- a/changelogs/unreleased/jivl-realtime-update-adding-file.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add realtime pipeline status for adding/viewing files
-merge_request: 17705
-author:
-type: other
diff --git a/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml b/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml
deleted file mode 100644
index c5cdbcf7b40..00000000000
--- a/changelogs/unreleased/jivl-summary-statistics-prometheus-dashboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add average and maximum summary statistics to the prometheus dashboard
-merge_request: 17921
-author:
-type: changed
diff --git a/changelogs/unreleased/jprovazn-fix-resolvable.yml b/changelogs/unreleased/jprovazn-fix-resolvable.yml
new file mode 100644
index 00000000000..e17c409e290
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-fix-resolvable.yml
@@ -0,0 +1,5 @@
+---
+title: Fix resolvable check if note's commit could not be found.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-issueref.yml b/changelogs/unreleased/jprovazn-issueref.yml
deleted file mode 100644
index ee19cac7b19..00000000000
--- a/changelogs/unreleased/jprovazn-issueref.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Display state indicator for issuable references in non-project scope (e.g.
- when referencing issuables from group scope).
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/jprovazn-null-byte.yml b/changelogs/unreleased/jprovazn-null-byte.yml
new file mode 100644
index 00000000000..4c4760ac412
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-null-byte.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filename matching when processing file or blob search results
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-pipeline-policy.yml b/changelogs/unreleased/jprovazn-pipeline-policy.yml
new file mode 100644
index 00000000000..2997c6c8667
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-pipeline-policy.yml
@@ -0,0 +1,6 @@
+---
+title: Allow maintainers to retry pipelines on forked projects (if allowed in merge
+ request)
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-remote-upload-destroy.yml b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
new file mode 100644
index 00000000000..22e55920fa3
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-remote-upload-destroy.yml
@@ -0,0 +1,5 @@
+---
+title: Fix deletion of Object Store uploads
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jr-web-ide-shortcuts.yml b/changelogs/unreleased/jr-web-ide-shortcuts.yml
new file mode 100644
index 00000000000..a895eab432a
--- /dev/null
+++ b/changelogs/unreleased/jr-web-ide-shortcuts.yml
@@ -0,0 +1,5 @@
+---
+title: Add shortcuts to Web IDE docs and modal
+merge_request: 19044
+author:
+type: changed
diff --git a/changelogs/unreleased/jramsay-38830-tarball.yml b/changelogs/unreleased/jramsay-38830-tarball.yml
deleted file mode 100644
index 6d40c305614..00000000000
--- a/changelogs/unreleased/jramsay-38830-tarball.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add alternate archive route for simplified packaging
-merge_request: 17225
-author:
-type: added
diff --git a/changelogs/unreleased/memoize-database-version.yml b/changelogs/unreleased/memoize-database-version.yml
new file mode 100644
index 00000000000..575348a53a1
--- /dev/null
+++ b/changelogs/unreleased/memoize-database-version.yml
@@ -0,0 +1,5 @@
+---
+title: Memoize Gitlab::Database.version
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/merge-request-widget-source-branch-improvements.yml b/changelogs/unreleased/merge-request-widget-source-branch-improvements.yml
deleted file mode 100644
index 942eb6062fd..00000000000
--- a/changelogs/unreleased/merge-request-widget-source-branch-improvements.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fixes remove source branch checkbox being visible when user cannot remove the
- branch
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml b/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml
new file mode 100644
index 00000000000..59f375de20e
--- /dev/null
+++ b/changelogs/unreleased/migrate-restore-repo-to-gitaly.yml
@@ -0,0 +1,5 @@
+---
+title: Support restoring repositories into gitaly
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/move-board-blank-state-vue-component.yml b/changelogs/unreleased/move-board-blank-state-vue-component.yml
deleted file mode 100644
index 0a278a8c009..00000000000
--- a/changelogs/unreleased/move-board-blank-state-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move BoardBlankState vue component
-merge_request: 17666
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-disussion-actions-to-the-right.yml b/changelogs/unreleased/move-disussion-actions-to-the-right.yml
new file mode 100644
index 00000000000..b79c6f36585
--- /dev/null
+++ b/changelogs/unreleased/move-disussion-actions-to-the-right.yml
@@ -0,0 +1,5 @@
+---
+title: Move discussion actions to the right for small viewports
+merge_request: 18476
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/move-email-footer-info-to-single-line.yml b/changelogs/unreleased/move-email-footer-info-to-single-line.yml
deleted file mode 100644
index 87ed5638056..00000000000
--- a/changelogs/unreleased/move-email-footer-info-to-single-line.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move email footer info to a single line
-merge_request: 17916
-author:
-type: changed
diff --git a/changelogs/unreleased/move-estimate-only-pane-vue-component.yml b/changelogs/unreleased/move-estimate-only-pane-vue-component.yml
deleted file mode 100644
index b6c538f70b3..00000000000
--- a/changelogs/unreleased/move-estimate-only-pane-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingEstimateOnlyPane vue component
-merge_request: 18318
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-help-state-vue-component.yml b/changelogs/unreleased/move-help-state-vue-component.yml
deleted file mode 100644
index 6108368cde0..00000000000
--- a/changelogs/unreleased/move-help-state-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingHelpState vue component
-merge_request: 18319
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-pipeline-failed-vue-component.yml b/changelogs/unreleased/move-pipeline-failed-vue-component.yml
deleted file mode 100644
index 38d42134876..00000000000
--- a/changelogs/unreleased/move-pipeline-failed-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move PipelineFailed vue component
-merge_request: 18277
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml b/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
deleted file mode 100644
index 03a6fd42228..00000000000
--- a/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
- title: Move 'Registry' after 'CI/CD' in project navigation sidebar
- merge_request: 18018
- author: Elias Werberich
- type: changed
diff --git a/changelogs/unreleased/mr-conflict-notification.yml b/changelogs/unreleased/mr-conflict-notification.yml
new file mode 100644
index 00000000000..d3d5f1fc373
--- /dev/null
+++ b/changelogs/unreleased/mr-conflict-notification.yml
@@ -0,0 +1,5 @@
+---
+title: When MR becomes unmergeable, notify and create todo for author and merge user
+merge_request: 18042
+author:
+type: added
diff --git a/changelogs/unreleased/optional-api-delimiter.yml b/changelogs/unreleased/optional-api-delimiter.yml
deleted file mode 100644
index 0bcd0787306..00000000000
--- a/changelogs/unreleased/optional-api-delimiter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make /-/ delimiter optional for search endpoints
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml b/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml
deleted file mode 100644
index 44973641325..00000000000
--- a/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Render MR commit SHA instead "diffs" when viable
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml b/changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml
deleted file mode 100644
index 978c5468bb1..00000000000
--- a/changelogs/unreleased/osw-44295-adjust-authorization-for-discussions-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adjust 404's for LegacyDiffNote discussion rendering
-merge_request: 18201
-author:
-type: fixed
diff --git a/changelogs/unreleased/pages_force_https.yml b/changelogs/unreleased/pages_force_https.yml
deleted file mode 100644
index da7e29087f3..00000000000
--- a/changelogs/unreleased/pages_force_https.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add HTTPS-only pages
-merge_request: 16273
-author: rfwatson
-type: added
diff --git a/changelogs/unreleased/pipelines-index-performance.yml b/changelogs/unreleased/pipelines-index-performance.yml
new file mode 100644
index 00000000000..928c2ddab72
--- /dev/null
+++ b/changelogs/unreleased/pipelines-index-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of project pipelines pages
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/poc-upload-hashing-path.yml b/changelogs/unreleased/poc-upload-hashing-path.yml
deleted file mode 100644
index 7970405bea1..00000000000
--- a/changelogs/unreleased/poc-upload-hashing-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: File uploads in remote storage now support project renaming.
-merge_request: 4597
-author:
-type: fixed
diff --git a/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
deleted file mode 100644
index 1f793fe5e7c..00000000000
--- a/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reduce number of queries when viewing a merge request
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/refactor-move-assignee-title-vue-component.yml b/changelogs/unreleased/refactor-move-assignee-title-vue-component.yml
deleted file mode 100644
index f6521339c39..00000000000
--- a/changelogs/unreleased/refactor-move-assignee-title-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move AssigneeTitle vue component
-merge_request: 17397
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml b/changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml
deleted file mode 100644
index 96e63343963..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-memory-usage-and-graph-components.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move MemoryGraph and MemoryUsage vue components
-merge_request: 17533
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml
deleted file mode 100644
index dc8ff95dc27..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-nothing-to-merge-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move NothingToMerge vue component
-merge_request: 17544
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml
deleted file mode 100644
index 90192fae030..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-ready-to-merge-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move ReadyToMerge vue component
-merge_request: 17545
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml
deleted file mode 100644
index ac41fe23d3d..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move ShaMismatch vue component
-merge_request: 17546
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml
deleted file mode 100644
index a31f1f372a8..00000000000
--- a/changelogs/unreleased/refactor-move-mr-widget-unresolved-discussions-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move UnresolvedDiscussions vue component
-merge_request: 17538
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
new file mode 100644
index 00000000000..b8b2762a21d
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-squash-before-merge-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move SquashBeforeMerge vue component
+merge_request: 18813
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml b/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml
deleted file mode 100644
index 88a4b8ec8c1..00000000000
--- a/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingComparisonPane vue component
-merge_request: 17931
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml b/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml
deleted file mode 100644
index 8151655250a..00000000000
--- a/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move TimeTrackingCollapsedState vue component
-merge_request: 17399
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml b/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml
new file mode 100644
index 00000000000..ddf7f51aa5e
--- /dev/null
+++ b/changelogs/unreleased/registry-ux-improvements-remove-clipboard-prefix.yml
@@ -0,0 +1,5 @@
+---
+title: Remove docker pull prefix from registry clipboard feature
+merge_request: 18933
+author: Lars Greiss
+type: changed
diff --git a/changelogs/unreleased/remove-pages-tar-support.yml b/changelogs/unreleased/remove-pages-tar-support.yml
deleted file mode 100644
index 73448687912..00000000000
--- a/changelogs/unreleased/remove-pages-tar-support.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove support for legacy tar.gz pages artifacts
-merge_request: 18090
-author:
-type: deprecated
diff --git a/changelogs/unreleased/rename-merge-request-widget-author-component.yml b/changelogs/unreleased/rename-merge-request-widget-author-component.yml
new file mode 100644
index 00000000000..15e6eafd826
--- /dev/null
+++ b/changelogs/unreleased/rename-merge-request-widget-author-component.yml
@@ -0,0 +1,5 @@
+---
+title: Rename merge request widget author component
+merge_request: 19079
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/rename-overview-project-sidenav.yml b/changelogs/unreleased/rename-overview-project-sidenav.yml
deleted file mode 100644
index 3632ef25c00..00000000000
--- a/changelogs/unreleased/rename-overview-project-sidenav.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Renamed Overview to Project in the contextual navigation at a project level
-merge_request: 18295
-author: Constance Okoghenun
-type: changed
diff --git a/changelogs/unreleased/rendering-markdown-multiple-projects.yml b/changelogs/unreleased/rendering-markdown-multiple-projects.yml
deleted file mode 100644
index 8685772c089..00000000000
--- a/changelogs/unreleased/rendering-markdown-multiple-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support Markdown rendering using multiple projects
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml b/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml
deleted file mode 100644
index 81b48fc255b..00000000000
--- a/changelogs/unreleased/sh-add-cleanup-rpc-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automatically cleanup stale worktrees and lock files upon a push
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-appearance-cache-key-version.yml b/changelogs/unreleased/sh-appearance-cache-key-version.yml
deleted file mode 100644
index c17a3dc36cd..00000000000
--- a/changelogs/unreleased/sh-appearance-cache-key-version.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use the GitLab version as part of the appearances cache key
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
new file mode 100644
index 00000000000..aae42b66c84
--- /dev/null
+++ b/changelogs/unreleased/sh-enforce-unique-and-not-null-project-ids-project-features.yml
@@ -0,0 +1,5 @@
+---
+title: Add a unique and not null constraint on the project_features.project_id column
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml b/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml
new file mode 100644
index 00000000000..d9bd1af9380
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml
@@ -0,0 +1,5 @@
+---
+title: Fix admin counters not working when PostgreSQL has secondaries
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml b/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml
new file mode 100644
index 00000000000..71b121710ee
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-backup-specific-rake-task.yml
@@ -0,0 +1,5 @@
+---
+title: Fix backup creation and restore for specific Rake tasks
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
new file mode 100644
index 00000000000..3c51aaae896
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-cross-site-origin-uploads-js.yml
@@ -0,0 +1,5 @@
+---
+title: Fix cross-origin errors when attempting to download JavaScript attachments
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-grape-logging-status-code.yml b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
new file mode 100644
index 00000000000..aabf9a84bfb
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-grape-logging-status-code.yml
@@ -0,0 +1,5 @@
+---
+title: Fix api_json.log not always reporting the right HTTP status code
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml b/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml
deleted file mode 100644
index f68d45d2f38..00000000000
--- a/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for Sidekiq JSON logging
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/sh-memoize-repository-empty.yml b/changelogs/unreleased/sh-memoize-repository-empty.yml
deleted file mode 100644
index 64db3ca0371..00000000000
--- a/changelogs/unreleased/sh-memoize-repository-empty.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Memoize Git::Repository#has_visible_content?
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-move-delete-groups-api-async.yml b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
new file mode 100644
index 00000000000..1b200cac5c5
--- /dev/null
+++ b/changelogs/unreleased/sh-move-delete-groups-api-async.yml
@@ -0,0 +1,5 @@
+---
+title: Move API group deletion to Sidekiq
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml b/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml
deleted file mode 100644
index 1990f4f6124..00000000000
--- a/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move Sidekiq exporter logs to log/sidekiq_exporter.log
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml b/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
new file mode 100644
index 00000000000..6c378fd450a
--- /dev/null
+++ b/changelogs/unreleased/support-active-setting-while-registering-a-runner.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for 'active' setting on Runner Registration API endpoint
+merge_request: 18848
+author:
+type: changed
diff --git a/changelogs/unreleased/tc-re-add-read-only-banner.yml b/changelogs/unreleased/tc-re-add-read-only-banner.yml
deleted file mode 100644
index 35bcd7e184e..00000000000
--- a/changelogs/unreleased/tc-re-add-read-only-banner.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add read-only banner to all pages
-merge_request: 17798
-author:
-type: fixed
diff --git a/changelogs/unreleased/ui-mr-counter-cache.yml b/changelogs/unreleased/ui-mr-counter-cache.yml
deleted file mode 100644
index 5e241bfe93f..00000000000
--- a/changelogs/unreleased/ui-mr-counter-cache.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Deleting a MR you are assigned to should decrements counter
-merge_request: 17951
-author: m b
-type: fixed
diff --git a/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml b/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml
deleted file mode 100644
index d99a9c93c0b..00000000000
--- a/changelogs/unreleased/unresolved-discussions-vue-component-i18n-and-tests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add i18n and update specs for UnresolvedDiscussions vue component
-merge_request: 17866
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml b/changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml
deleted file mode 100644
index c76495ec959..00000000000
--- a/changelogs/unreleased/update-gitlab-ci-yml-services-docs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update CI services documnetation
-merge_request: 17749
-author:
-type: other
diff --git a/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml b/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml
deleted file mode 100644
index 9c13bfbaf6f..00000000000
--- a/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update spec import path for vue mount component helper
-merge_request: 17880
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/update-unresolved-discussions-vue-component.yml b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
deleted file mode 100644
index 246eaaae2bd..00000000000
--- a/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add i18n and update specs for ShaMismatch vue component
-merge_request: 17870
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/update-wiki-modal.yml b/changelogs/unreleased/update-wiki-modal.yml
new file mode 100644
index 00000000000..00f2fc4f181
--- /dev/null
+++ b/changelogs/unreleased/update-wiki-modal.yml
@@ -0,0 +1,5 @@
+---
+title: New design for wiki page deletion confirmation
+merge_request: 18712
+author: Constance Okoghenun
+type: added
diff --git a/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
new file mode 100644
index 00000000000..098e4b1d5fa
--- /dev/null
+++ b/changelogs/unreleased/use-case-insensitive-ordering-for-dashboard-filters.yml
@@ -0,0 +1,5 @@
+---
+title: "Use case in-sensitive ordering by name for dashboard"
+merge_request: 18553
+author: "@vedharish"
+type: fixed
diff --git a/changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml b/changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml
deleted file mode 100644
index 675d347b64c..00000000000
--- a/changelogs/unreleased/use-chronic-duration-attribute-for-project-build-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use human readable value build_timeout in Project
-merge_request: 17386
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml b/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml
deleted file mode 100644
index 14114eca2b2..00000000000
--- a/changelogs/unreleased/winh-41174-projects-groups-badges-ui.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Projects and groups badges settings UI
-merge_request: 17114
-author:
-type: added
diff --git a/changelogs/unreleased/winh-deprecate-old-modal.yml b/changelogs/unreleased/winh-deprecate-old-modal.yml
deleted file mode 100644
index 4fae1fafbea..00000000000
--- a/changelogs/unreleased/winh-deprecate-old-modal.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rename modal.vue to deprecated_modal.vue
-merge_request: 17438
-author:
-type: other
diff --git a/changelogs/unreleased/winh-dropdown-entry-unlocking.yml b/changelogs/unreleased/winh-dropdown-entry-unlocking.yml
deleted file mode 100644
index fc669af1f57..00000000000
--- a/changelogs/unreleased/winh-dropdown-entry-unlocking.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove green background from unlock button in admin area
-merge_request: 18288
-author:
-type: changed
diff --git a/changelogs/unreleased/winh-make-it-right-now.yml b/changelogs/unreleased/winh-make-it-right-now.yml
new file mode 100644
index 00000000000..7b386c0b332
--- /dev/null
+++ b/changelogs/unreleased/winh-make-it-right-now.yml
@@ -0,0 +1,5 @@
+---
+title: Use "right now" for short time periods
+merge_request: 19095
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-new-merge-request-encoding.yml b/changelogs/unreleased/winh-new-merge-request-encoding.yml
new file mode 100644
index 00000000000..f797657e660
--- /dev/null
+++ b/changelogs/unreleased/winh-new-merge-request-encoding.yml
@@ -0,0 +1,5 @@
+---
+title: Fix encoding of branch names on compare and new merge request page
+merge_request: 19143
+author:
+type: fixed
diff --git a/changelogs/unreleased/workhorse-gitaly-mandatory.yml b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
deleted file mode 100644
index 77b62302e86..00000000000
--- a/changelogs/unreleased/workhorse-gitaly-mandatory.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make all workhorse gitaly calls opt-out, take 2
-merge_request: 18043
-author:
-type: other
diff --git a/changelogs/unreleased/zj-add-branch-mandatory.yml b/changelogs/unreleased/zj-add-branch-mandatory.yml
new file mode 100644
index 00000000000..82712ce842d
--- /dev/null
+++ b/changelogs/unreleased/zj-add-branch-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Adding branches through the WebUI is handled by Gitaly
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml b/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml
deleted file mode 100644
index 3d11ee588ae..00000000000
--- a/changelogs/unreleased/zj-branch-containing-sha-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Detecting branchnames containing a commit uses Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-bump-gitaly.yml b/changelogs/unreleased/zj-bump-gitaly.yml
deleted file mode 100644
index eb28bed70e4..00000000000
--- a/changelogs/unreleased/zj-bump-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade Gitaly to upgrade its charlock_holmes
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/zj-calculate-checksum-mandator.yml b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
new file mode 100644
index 00000000000..83315a3c5dd
--- /dev/null
+++ b/changelogs/unreleased/zj-calculate-checksum-mandator.yml
@@ -0,0 +1,5 @@
+---
+title: Remove shellout implementation for Repository checksums
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-feature-gate-remove-http-api.yml b/changelogs/unreleased/zj-feature-gate-remove-http-api.yml
deleted file mode 100644
index 2095f60146c..00000000000
--- a/changelogs/unreleased/zj-feature-gate-remove-http-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow feature gates to be removed through the API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/zj-find-license-opt-out.yml b/changelogs/unreleased/zj-find-license-opt-out.yml
deleted file mode 100644
index be2656601a9..00000000000
--- a/changelogs/unreleased/zj-find-license-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Detect repository license on Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-opt-out-delete-refs.yml b/changelogs/unreleased/zj-opt-out-delete-refs.yml
deleted file mode 100644
index b02a45eee17..00000000000
--- a/changelogs/unreleased/zj-opt-out-delete-refs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bulk deleting refs is handled by Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml b/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml
deleted file mode 100644
index 3871293ee04..00000000000
--- a/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: ListCommitsByOid is executed by Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml b/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml
new file mode 100644
index 00000000000..61bdce43c0e
--- /dev/null
+++ b/changelogs/unreleased/zj-ref-contains-sha-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Refs containting sha checks are done by Gitaly
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-ref-exists-opt-out.yml b/changelogs/unreleased/zj-ref-exists-opt-out.yml
deleted file mode 100644
index cdffecb0d0a..00000000000
--- a/changelogs/unreleased/zj-ref-exists-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Check if a ref exists is done by Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-remote-repo-exists.yml b/changelogs/unreleased/zj-remote-repo-exists.yml
deleted file mode 100644
index f024b83159b..00000000000
--- a/changelogs/unreleased/zj-remote-repo-exists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Test if remote repository exists when importing wikis
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml b/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml
deleted file mode 100644
index 4774c7811d1..00000000000
--- a/changelogs/unreleased/zj-tag-containing-sha-opt-out.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Detecting tags containing a commit uses Gitaly by default
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
new file mode 100644
index 00000000000..5af53c56017
--- /dev/null
+++ b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Finding a wiki page is done by Gitaly by default
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-workhorse-archive-mandatory.yml b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
new file mode 100644
index 00000000000..3a4a351a2b9
--- /dev/null
+++ b/changelogs/unreleased/zj-workhorse-archive-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Workhorse will use Gitaly to create archives
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
new file mode 100644
index 00000000000..bce68692d98
--- /dev/null
+++ b/changelogs/unreleased/zj-workhorse-commit-patch-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Workhorse to send raw diff and patch for commits
+merge_request:
+author:
+type: other
diff --git a/config/application.rb b/config/application.rb
index ad7338763f7..09f706e3d70 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -115,6 +115,7 @@ module Gitlab
config.assets.precompile << "test.css"
config.assets.precompile << "snippets.css"
config.assets.precompile << "locale/**/app.js"
+ config.assets.precompile << "emoji_sprites.css"
# Import gitlab-svgs directly from vendored directory
config.assets.paths << "#{config.root}/node_modules/@gitlab-org/gitlab-svgs/dist"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 8c39a1f2aa9..7eb44b8059e 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -184,18 +184,18 @@ production: &base
# base_dir: uploads/-/system
object_store:
enabled: false
- # remote_directory: uploads # Bucket name
+ remote_directory: uploads # Bucket name
# direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false)
# background_upload: false # Temporary option to limit automatic upload (Default: true)
# proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
- connection:
- provider: AWS
- aws_access_key_id: AWS_ACCESS_KEY_ID
- aws_secret_access_key: AWS_SECRET_ACCESS_KEY
- region: us-east-1
- # host: 'localhost' # default: s3.amazonaws.com
- # endpoint: 'http://127.0.0.1:9000' # default: nil
- # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
+ connection:
+ provider: AWS
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: us-east-1
+ # host: 'localhost' # default: s3.amazonaws.com
+ # endpoint: 'http://127.0.0.1:9000' # default: nil
+ # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## GitLab Pages
pages:
@@ -212,6 +212,8 @@ production: &base
artifacts_server: true
# external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
+ admin:
+ address: unix:/home/git/gitlab/tmp/sockets/private/pages-admin.socket # TCP connections are supported too (e.g. tcp://host:port)
## Mattermost
## For enabling Add to Mattermost button
@@ -532,7 +534,7 @@ production: &base
# required_claims: ["name", "email"],
# info_map: { name: "name", email: "email" },
# auth_url: 'https://example.com/',
- # valid_within: nil,
+ # valid_within: null,
# }
# }
# - { name: 'saml',
@@ -823,7 +825,7 @@ test:
required_claims: ["name", "email"],
info_map: { name: "name", email: "email" },
auth_url: 'https://example.com/',
- valid_within: nil,
+ valid_within: null,
}
}
- { name: 'auth0',
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index dc7999ac556..dd36700964a 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -1,131 +1,4 @@
-# rubocop:disable GitlabSecurity/PublicSend
-
-require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
-
-class Settings < Settingslogic
- source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
- namespace Rails.env
-
- class << self
- def gitlab_on_standard_port?
- on_standard_port?(gitlab)
- end
-
- def host_without_www(url)
- host(url).sub('www.', '')
- end
-
- def build_gitlab_ci_url
- custom_port =
- if on_standard_port?(gitlab)
- nil
- else
- ":#{gitlab.port}"
- end
-
- [
- gitlab.protocol,
- "://",
- gitlab.host,
- custom_port,
- gitlab.relative_url_root
- ].join('')
- end
-
- def build_pages_url
- base_url(pages).join('')
- end
-
- def build_gitlab_shell_ssh_path_prefix
- user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
-
- if gitlab_shell.ssh_port != 22
- "ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
- else
- if gitlab_shell.ssh_host.include? ':'
- "[#{user_host}]:"
- else
- "#{user_host}:"
- end
- end
- end
-
- def build_base_gitlab_url
- base_url(gitlab).join('')
- end
-
- def build_gitlab_url
- (base_url(gitlab) + [gitlab.relative_url_root]).join('')
- end
-
- # check that values in `current` (string or integer) is a contant in `modul`.
- def verify_constant_array(modul, current, default)
- values = default || []
- unless current.nil?
- values = []
- current.each do |constant|
- values.push(verify_constant(modul, constant, nil))
- end
- values.delete_if { |value| value.nil? }
- end
-
- values
- end
-
- # check that `current` (string or integer) is a contant in `modul`.
- def verify_constant(modul, current, default)
- constant = modul.constants.find { |name| modul.const_get(name) == current }
- value = constant.nil? ? default : modul.const_get(constant)
- if current.is_a? String
- value = modul.const_get(current.upcase) rescue default
- end
-
- value
- end
-
- def absolute(path)
- File.expand_path(path, Rails.root)
- end
-
- private
-
- def base_url(config)
- custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
-
- [
- config.protocol,
- "://",
- config.host,
- custom_port
- ]
- end
-
- def on_standard_port?(config)
- config.port.to_i == (config.https ? 443 : 80)
- end
-
- # Extract the host part of the given +url+.
- def host(url)
- url = url.downcase
- url = "http://#{url}" unless url.start_with?('http')
-
- # Get rid of the path so that we don't even have to encode it
- url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
-
- URI.parse(url_without_path).host
- end
-
- # Runs every minute in a random ten-minute period on Sundays, to balance the
- # load on the server receiving these pings. The usage ping is safe to run
- # multiple times because of a 24 hour exclusive lock.
- def cron_for_usage_ping
- hour = rand(24)
- minute = rand(6)
-
- "#{minute}0-#{minute}9 #{hour} * * 0"
- end
- end
-end
+require_relative '../settings'
# Default settings
Settings['ldap'] ||= Settingslogic.new({})
@@ -342,6 +215,9 @@ Settings.pages['external_http'] ||= false unless Settings.pages['external_ht
Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pages['artifacts_server'].nil?
+Settings.pages['admin'] ||= Settingslogic.new({})
+Settings.pages.admin['certificate'] ||= ''
+
#
# Git LFS
#
@@ -594,6 +470,3 @@ if Rails.env.test?
Settings.gitlab['default_can_create_group'] = true
Settings.gitlab['default_can_create_team'] = false
end
-
-# Force a refresh of application settings at startup
-ApplicationSetting.expire
diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb
deleted file mode 100644
index bd74f90e7d2..00000000000
--- a/config/initializers/2_app.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module Gitlab
- def self.config
- Settings
- end
-
- VERSION = File.read(Rails.root.join("VERSION")).strip.freeze
- REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
-end
diff --git a/config/initializers/2_gitlab.rb b/config/initializers/2_gitlab.rb
new file mode 100644
index 00000000000..1d2ab606a63
--- /dev/null
+++ b/config/initializers/2_gitlab.rb
@@ -0,0 +1 @@
+require_relative '../../lib/gitlab'
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index d92cdb97766..89aabe530fe 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -26,17 +26,6 @@ def validate_storages_config
Gitlab.config.repositories.storages.each do |name, repository_storage|
storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
- if repository_storage.is_a?(String)
- raise "#{name} is not a valid storage, because it has no `path` key. " \
- "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \
- "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \
- "If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n"
- end
-
- if !repository_storage.is_a?(Gitlab::GitalyClient::StorageSettings) || repository_storage.legacy_disk_path.nil?
- storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
- end
-
%w(failure_count_threshold failure_reset_time storage_timeout).each do |setting|
# Falling back to the defaults is fine!
next if repository_storage[setting].nil?
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index eb7959e4da6..146c4b1e024 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -25,7 +25,7 @@ Sidekiq.configure_server do |config|
end
end
-if Gitlab::Metrics.prometheus_metrics_enabled?
+if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
unless Sidekiq.server?
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
end
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index 7cdf49159b4..8a851b89c56 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -119,7 +119,14 @@ def instrument_classes(instrumentation)
end
# rubocop:enable Metrics/AbcSize
-if Gitlab::Metrics.enabled?
+# With prometheus enabled by default this breaks all specs
+# that stubs methods using `any_instance_of` for the models reloaded here.
+#
+# We should deprecate the usage of `any_instance_of` in the future
+# 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?
require 'pathname'
require 'influxdb'
require 'connection_pool'
diff --git a/config/initializers/fast_gettext.rb b/config/initializers/9_fast_gettext.rb
index fd0167aa476..fd0167aa476 100644
--- a/config/initializers/fast_gettext.rb
+++ b/config/initializers/9_fast_gettext.rb
diff --git a/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb b/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
new file mode 100644
index 00000000000..d9418caf68b
--- /dev/null
+++ b/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
@@ -0,0 +1,98 @@
+# This is a monkey patch which must be removed when migrating to Rails 5.1 from 5.0.
+#
+# In Rails 5.0 there was introduced a bug which casts types in the uniqueness validator.
+# https://github.com/rails/rails/pull/23523/commits/811a4fa8eb6ceea841e61e8ac05747ffb69595ae
+#
+# That causes to bugs like this:
+#
+# 1) API::Users POST /user/:id/gpg_keys/:key_id/revoke when authenticated revokes existing key
+# Failure/Error: let(:gpg_key) { create(:gpg_key, user: user) }
+#
+# TypeError:
+# can't cast Hash
+# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
+# # ./spec/requests/api/users_spec.rb:908:in `block (4 levels) in <top (required)>'
+# # ------------------
+# # --- Caused by: ---
+# # TypeError:
+# # TypeError
+# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
+#
+# This bug was fixed in Rails 5.1 by https://github.com/rails/rails/pull/24745/commits/aa062318c451512035c10898a1af95943b1a3803
+
+if Gitlab.rails5?
+ ActiveSupport::Deprecation.warn("#{__FILE__} is a monkey patch which must be removed when upgrading to Rails 5.1")
+
+ if Rails.version.start_with?("5.1")
+ raise "Remove this monkey patch: #{__FILE__}"
+ end
+
+ # Copy-paste from https://github.com/kamipo/rails/blob/aa062318c451512035c10898a1af95943b1a3803/activerecord/lib/active_record/validations/uniqueness.rb
+ # including local fixes to make Rubocop happy again.
+ module ActiveRecord
+ module Validations
+ class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
+ def validate_each(record, attribute, value)
+ finder_class = find_finder_class_for(record)
+ table = finder_class.arel_table
+ value = map_enum_attribute(finder_class, attribute, value)
+
+ relation = build_relation(finder_class, table, attribute, value)
+
+ if record.persisted?
+ if finder_class.primary_key
+ relation = relation.where.not(finder_class.primary_key => record.id_was || record.id)
+ else
+ raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ end
+ end
+
+ relation = scope_relation(record, table, relation)
+ relation = relation.merge(options[:conditions]) if options[:conditions]
+
+ if relation.exists?
+ error_options = options.except(:case_sensitive, :scope, :conditions)
+ error_options[:value] = value
+
+ record.errors.add(attribute, :taken, error_options)
+ end
+ rescue RangeError
+ end
+
+ protected
+
+ def build_relation(klass, table, attribute, value) #:nodoc:
+ if reflection = klass._reflect_on_association(attribute)
+ attribute = reflection.foreign_key
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
+ end
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_alias?(attribute)
+ attribute = klass.attribute_alias(attribute)
+ end
+
+ attribute_name = attribute.to_s
+
+ column = klass.columns_hash[attribute_name]
+ cast_type = klass.type_for_attribute(attribute_name)
+
+ comparison =
+ if !options[:case_sensitive] && !value.nil?
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
+ klass.connection.case_insensitive_comparison(table, attribute, column, value)
+ else
+ klass.connection.case_sensitive_comparison(table, attribute, column, value)
+ end
+
+ if value.nil?
+ klass.unscoped.where(comparison)
+ else
+ bind = Relation::QueryAttribute.new(attribute_name, value, cast_type)
+ klass.unscoped.where(comparison, bind)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/console_message.rb b/config/initializers/console_message.rb
new file mode 100644
index 00000000000..2c46a25f365
--- /dev/null
+++ b/config/initializers/console_message.rb
@@ -0,0 +1,10 @@
+# rubocop:disable Rails/Output
+if defined?(Rails::Console)
+ # note that this will not print out when using `spring`
+ justify = 15
+ puts "-------------------------------------------------------------------------------------"
+ puts " Gitlab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})"
+ puts " Gitlab Shell:".ljust(justify) + Gitlab::Shell.new.version
+ puts " #{Gitlab::Database.adapter_name}:".ljust(justify) + Gitlab::Database.version
+ puts "-------------------------------------------------------------------------------------"
+end
diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb
index f3f47b2ccf0..14616e726d9 100644
--- a/config/initializers/deprecations.rb
+++ b/config/initializers/deprecations.rb
@@ -1,5 +1,9 @@
-deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
+if Rails.env.development? || ENV['GITLAB_LEGACY_PATH_LOG_MESSAGE']
+ deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
+
+ deprecator.behavior = -> (message, callstack) {
+ Rails.logger.warn("#{message}: #{callstack[1..20].join}")
+ }
-if Gitlab.com? || Rails.env.development?
ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator)
end
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 2079d3acb72..e3a342590d4 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -104,5 +104,5 @@ Doorkeeper.configure do
# set to true if you want this to be allowed
# wildcard_redirect_uri false
- base_controller 'ApplicationController'
+ base_controller '::Gitlab::BaseDoorkeeperController'
end
diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb
index c60ad535fd5..80cab7273e5 100644
--- a/config/initializers/flipper.rb
+++ b/config/initializers/flipper.rb
@@ -1,22 +1 @@
-require 'flipper/adapters/active_record'
-require 'flipper/adapters/active_support_cache_store'
-
-Flipper.configure do |config|
- config.default do
- adapter = Flipper::Adapters::ActiveRecord.new(
- feature_class: Feature::FlipperFeature, gate_class: Feature::FlipperGate)
- cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new(
- adapter,
- Rails.cache,
- expires_in: 1.hour)
-
- Flipper.new(cached_adapter)
- end
-end
-
Feature.register_feature_groups
-
-unless Rails.env.test?
- require 'flipper/middleware/memoizer'
- Rails.application.config.middleware.use Flipper::Middleware::Memoizer
-end
diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb
index 4603123665d..deb94d7dbce 100644
--- a/config/initializers/forbid_sidekiq_in_transactions.rb
+++ b/config/initializers/forbid_sidekiq_in_transactions.rb
@@ -27,7 +27,7 @@ module Sidekiq
Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead.
MSG
rescue Sidekiq::Worker::EnqueueFromTransactionError => e
- Rails.logger.error(e.message) if Rails.env.production?
+ ::Rails.logger.error(e.message) if ::Rails.env.production?
Gitlab::Sentry.track_exception(e)
end
end
diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb
index 81e0577a7c9..ea9cc151a57 100644
--- a/config/initializers/gollum.rb
+++ b/config/initializers/gollum.rb
@@ -7,6 +7,20 @@ module Gollum
end
require "gollum-lib"
+module Gollum
+ class Page
+ def text_data(encoding = nil)
+ data = if raw_data.respond_to?(:encoding)
+ raw_data.force_encoding(encoding || Encoding::UTF_8)
+ else
+ raw_data
+ end
+
+ Gitlab::EncodingHelper.encode!(data)
+ end
+ end
+end
+
Rails.application.configure do
config.after_initialize do
Gollum::Page.per_page = Kaminari.config.default_per_page
diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb
new file mode 100644
index 00000000000..7f115268b37
--- /dev/null
+++ b/config/initializers/kubeclient.rb
@@ -0,0 +1,16 @@
+class Kubeclient::Client
+ # We need to monkey patch this method until
+ # https://github.com/abonas/kubeclient/pull/323 is merged
+ def proxy_url(kind, name, port, namespace = '')
+ discover unless @discovered
+ entity_name_plural =
+ if %w[services pods nodes].include?(kind.to_s)
+ kind.to_s
+ else
+ @entities[kind.to_s].resource_name
+ end
+
+ ns_prefix = build_namespace_prefix(namespace)
+ rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
+ end
+end
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 49fdd23064c..114c1cb512f 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -1,21 +1,3 @@
-# Monkey patch lograge until https://github.com/roidrage/lograge/pull/241 is released
-module Lograge
- class RequestLogSubscriber < ActiveSupport::LogSubscriber
- def strip_query_string(path)
- index = path.index('?')
- index ? path[0, index] : path
- end
-
- def extract_location
- location = Thread.current[:lograge_location]
- return {} unless location
-
- Thread.current[:lograge_location] = nil
- { location: strip_query_string(location) }
- end
- end
-end
-
# Only use Lograge for Rails
unless Sidekiq.server?
filename = File.join(Rails.root, 'log', "#{Rails.env}_json.log")
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 00baea08613..e33ebb25c4c 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -25,5 +25,6 @@ end
module OmniAuth
module Strategies
autoload :Bitbucket, Rails.root.join('lib', 'omni_auth', 'strategies', 'bitbucket')
+ autoload :Jwt, Rails.root.join('lib', 'omni_auth', 'strategies', 'jwt')
end
end
diff --git a/config/initializers/pages.rb b/config/initializers/pages.rb
new file mode 100644
index 00000000000..835197557e8
--- /dev/null
+++ b/config/initializers/pages.rb
@@ -0,0 +1,2 @@
+Gitlab::PagesClient.read_or_create_token
+Gitlab::PagesClient.load_certificate
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index ba04a2bf5fa..bc9b52ceef7 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -1,7 +1,6 @@
Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis::Cache.params) }
Peek.into Peek::Views::Host
-Peek.into Peek::Views::PerformanceBar
if Gitlab::Database.mysql?
require 'peek-mysql2'
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index b2da3b3dc19..17d09293205 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -13,7 +13,7 @@ def configure_sentry
if sentry_enabled
Raven.configure do |config|
config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn
- config.release = Gitlab::REVISION
+ config.release = Gitlab.revision
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index f2fde1e0048..da24881885e 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -15,19 +15,15 @@ cookie_key = if Rails.env.development?
"_gitlab_session"
end
-if Rails.env.test?
- Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
-else
- sessions_config = Gitlab::Redis::SharedState.params
- sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
+sessions_config = Gitlab::Redis::SharedState.params
+sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE
- Gitlab::Application.config.session_store(
- :redis_store, # Using the cookie_store would enable session replay attacks.
- servers: sessions_config,
- key: cookie_key,
- secure: Gitlab.config.gitlab.https,
- httponly: true,
- expires_in: Settings.gitlab['session_expire_delay'] * 60,
- path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
- )
-end
+Gitlab::Application.config.session_store(
+ :redis_store, # Using the cookie_store would enable session replay attacks.
+ servers: sessions_config,
+ key: cookie_key,
+ secure: Gitlab.config.gitlab.https,
+ httponly: true,
+ expires_in: Settings.gitlab['session_expire_delay'] * 60,
+ path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root
+)
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index d3a7a2b9f8b..6c28686e69a 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -34,7 +34,7 @@ if app.config.serve_static_files
)
app.config.middleware.insert_before(
Gitlab::Middleware::Static,
- Gitlab::Middleware::WebpackProxy,
+ Gitlab::Webpack::DevServerMiddleware,
proxy_path: app.config.webpack.public_path,
proxy_host: dev_server.host,
proxy_port: dev_server.port
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index 0c32528311e..ca2eed664ed 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -22,3 +22,16 @@ end.compact
Rails.application.config.action_dispatch.trusted_proxies = (
['127.0.0.1', '::1'] + gitlab_trusted_proxies)
+
+# A monkey patch to make trusted proxies work with Rails 5.0.
+# Inspired by https://github.com/rails/rails/issues/5223#issuecomment-263778719
+# Remove this monkey patch when upstream is fixed.
+if Gitlab.rails5?
+ module TrustedProxyMonkeyPatch
+ def ip
+ @ip ||= (get_header("action_dispatch.remote_ip") || super).to_s
+ end
+ end
+
+ ActionDispatch::Request.send(:include, TrustedProxyMonkeyPatch)
+end
diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb
index ee034d21eae..8cc36820d3c 100644
--- a/config/initializers/warden.rb
+++ b/config/initializers/warden.rb
@@ -1,9 +1,21 @@
Rails.application.configure do |config|
- Warden::Manager.after_set_user do |user, auth, opts|
+ Warden::Manager.after_set_user(scope: :user) do |user, auth, opts|
Gitlab::Auth::UniqueIpsLimiter.limit_user!(user)
end
- Warden::Manager.before_failure do |env, opts|
+ Warden::Manager.before_failure(scope: :user) do |env, opts|
Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env)
end
+
+ Warden::Manager.after_authentication(scope: :user) do |user, auth, opts|
+ ActiveSession.cleanup(user)
+ end
+
+ Warden::Manager.after_set_user(scope: :user, only: :fetch) do |user, auth, opts|
+ ActiveSession.set(user, auth.request)
+ end
+
+ Warden::Manager.before_logout(scope: :user) do |user, auth, opts|
+ ActiveSession.destroy(user || auth.user, auth.request.session.id)
+ end
end
diff --git a/config/karma.config.js b/config/karma.config.js
index 61f02294157..28a688797d9 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -1,47 +1,87 @@
-var path = require('path');
-var webpack = require('webpack');
-var argumentsParser = require('commander');
-var webpackConfig = require('./webpack.config.js');
-var ROOT_PATH = path.resolve(__dirname, '..');
-
-// remove problematic plugins
-if (webpackConfig.plugins) {
- webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) {
- return !(
- plugin instanceof webpack.optimize.CommonsChunkPlugin ||
- plugin instanceof webpack.optimize.ModuleConcatenationPlugin ||
- plugin instanceof webpack.DefinePlugin
- );
- });
+const path = require('path');
+const glob = require('glob');
+const chalk = require('chalk');
+const webpack = require('webpack');
+const argumentsParser = require('commander');
+const webpackConfig = require('./webpack.config.js');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+
+function fatalError(message) {
+ console.error(chalk.red(`\nError: ${message}\n`));
+ process.exit(1);
}
-var testFiles = argumentsParser
+// disable problematic options
+webpackConfig.entry = undefined;
+webpackConfig.mode = 'development';
+webpackConfig.optimization.runtimeChunk = false;
+webpackConfig.optimization.splitChunks = false;
+
+// use quicker sourcemap option
+webpackConfig.devtool = 'cheap-inline-source-map';
+
+const specFilters = argumentsParser
.option(
'-f, --filter-spec [filter]',
'Filter run spec files by path. Multiple filters are like a logical OR.',
- (val, memo) => {
- memo.push(val);
+ (filter, memo) => {
+ memo.push(filter, filter.replace(/\/?$/, '/**/*.js'));
return memo;
},
[]
)
.parse(process.argv).filterSpec;
-webpackConfig.plugins.push(
- new webpack.DefinePlugin({
- 'process.env.TEST_FILES': JSON.stringify(testFiles),
- })
-);
+if (specFilters.length) {
+ const specsPath = /^(?:\.[\\\/])?spec[\\\/]javascripts[\\\/]/;
-webpackConfig.devtool = 'cheap-inline-source-map';
+ // resolve filters
+ let filteredSpecFiles = specFilters.map(filter =>
+ glob
+ .sync(filter, {
+ root: ROOT_PATH,
+ matchBase: true,
+ })
+ .filter(path => path.endsWith('spec.js'))
+ );
+
+ // flatten
+ filteredSpecFiles = Array.prototype.concat.apply([], filteredSpecFiles);
+
+ // remove duplicates
+ filteredSpecFiles = [...new Set(filteredSpecFiles)];
+
+ if (filteredSpecFiles.length < 1) {
+ fatalError('Your filter did not match any test files.');
+ }
+
+ if (!filteredSpecFiles.every(file => specsPath.test(file))) {
+ fatalError('Test files must be located within /spec/javascripts.');
+ }
+
+ const newContext = filteredSpecFiles.reduce((context, file) => {
+ const relativePath = file.replace(specsPath, '');
+ context[file] = `./${relativePath}`;
+ return context;
+ }, {});
+
+ webpackConfig.plugins.push(
+ new webpack.ContextReplacementPlugin(
+ /spec[\\\/]javascripts$/,
+ path.join(ROOT_PATH, 'spec/javascripts'),
+ newContext
+ )
+ );
+}
// Karma configuration
module.exports = function(config) {
process.env.TZ = 'Etc/UTC';
- var progressReporter = process.env.CI ? 'mocha' : 'progress';
+ const progressReporter = process.env.CI ? 'mocha' : 'progress';
- var karmaConfig = {
+ const karmaConfig = {
basePath: ROOT_PATH,
browsers: ['ChromeHeadlessCustom'],
customLaunchers: {
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 10ca612b246..13732384953 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -103,10 +103,10 @@
- title: "Throughput"
y_label: "Requests / Sec"
required_metrics:
- - nginx_responses_total
+ - nginx_server_requests
weight: 1
queries:
- - query_range: 'sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code)'
+ - query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)'
unit: req / sec
label: Status Code
series:
@@ -121,19 +121,19 @@
- title: "Latency"
y_label: "Latency (ms)"
required_metrics:
- - nginx_upstream_response_msecs_avg
+ - nginx_server_requestMsec
weight: 1
queries:
- - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})'
+ - query_range: 'avg(nginx_server_requestMsec{%{environment_filter}})'
label: Upstream
unit: ms
- title: "HTTP Error Rate"
y_label: "HTTP 500 Errors / Sec"
required_metrics:
- - nginx_responses_total
+ - nginx_server_requests
weight: 1
queries:
- - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m]))'
+ - query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors
unit: "errors / sec"
- group: System metrics (Kubernetes)
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 170508e893d..7c4c3d370e0 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -58,6 +58,13 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
# On CE only index and show actions are needed
resources :boards, only: [:index, :show]
+
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ member do
+ post :resume
+ post :pause
+ end
+ end
end
scope(path: '*id',
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index bcfc17a5f66..a9ba5ac2c0b 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -30,6 +30,7 @@ resource :profile, only: [:show, :update] do
put :revoke
end
end
+ resources :active_sessions, only: [:index, :destroy]
resources :emails, only: [:index, :create, :destroy] do
member do
put :resend_confirmation_instructions
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 2a1bcb8cde2..5a1be1a8b73 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -161,7 +161,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
get :diff_for_path
- get :update_branches
get :branch_from
get :branch_to
end
@@ -175,6 +174,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ resource :mirror, only: [:show, :update] do
+ member do
+ post :update_now
+ end
+ end
+
resources :pipelines, only: [:index, :new, :create, :show] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
@@ -183,6 +188,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
member do
get :stage
+ get :stage_ajax
post :cancel
post :retry
get :builds
@@ -410,6 +416,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
collection do
post :toggle_shared_runners
+ post :toggle_group_runners
end
end
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 9e506a1a43a..e2bf8d6a7ff 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -18,6 +18,7 @@ scope format: false do
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
+ get :signatures
end
end
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 57fb37530bb..bc7df5e7584 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -1,3 +1,21 @@
+# Allows individual providers to be directed to a chosen controller
+# Call from inside devise_scope
+def override_omniauth(provider, controller, path_prefix = '/users/auth')
+ match "#{path_prefix}/#{provider}/callback",
+ to: "#{controller}##{provider}",
+ as: "#{provider}_omniauth_callback",
+ via: [:get, :post]
+end
+
+# Use custom controller for LDAP omniauth callback
+if Gitlab::Auth::LDAP::Config.enabled?
+ devise_scope :user do
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
+ override_omniauth(server['provider_name'], 'ldap/omniauth_callbacks')
+ end
+ end
+end
+
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
passwords: :passwords,
@@ -9,6 +27,13 @@ devise_scope :user do
get '/users/almost_there' => 'confirmations#almost_there'
end
+scope '-/users', module: :users do
+ resources :terms, only: [:index] do
+ post :accept, on: :member
+ post :decline, on: :member
+ end
+end
+
scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do
scope(path: 'users/:username',
as: :user,
diff --git a/config/settings.rb b/config/settings.rb
new file mode 100644
index 00000000000..69d637761ea
--- /dev/null
+++ b/config/settings.rb
@@ -0,0 +1,126 @@
+require 'settingslogic'
+
+class Settings < Settingslogic
+ source ENV.fetch('GITLAB_CONFIG') { Pathname.new(File.expand_path('..', __dir__)).join('config/gitlab.yml') }
+ namespace ENV.fetch('GITLAB_ENV') { Rails.env }
+
+ class << self
+ def gitlab_on_standard_port?
+ on_standard_port?(gitlab)
+ end
+
+ def host_without_www(url)
+ host(url).sub('www.', '')
+ end
+
+ def build_gitlab_ci_url
+ custom_port =
+ if on_standard_port?(gitlab)
+ nil
+ else
+ ":#{gitlab.port}"
+ end
+
+ [
+ gitlab.protocol,
+ "://",
+ gitlab.host,
+ custom_port,
+ gitlab.relative_url_root
+ ].join('')
+ end
+
+ def build_pages_url
+ base_url(pages).join('')
+ end
+
+ def build_gitlab_shell_ssh_path_prefix
+ user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
+
+ if gitlab_shell.ssh_port != 22
+ "ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
+ else
+ if gitlab_shell.ssh_host.include? ':'
+ "[#{user_host}]:"
+ else
+ "#{user_host}:"
+ end
+ end
+ end
+
+ def build_base_gitlab_url
+ base_url(gitlab).join('')
+ end
+
+ def build_gitlab_url
+ (base_url(gitlab) + [gitlab.relative_url_root]).join('')
+ end
+
+ # check that values in `current` (string or integer) is a contant in `modul`.
+ def verify_constant_array(modul, current, default)
+ values = default || []
+ unless current.nil?
+ values = []
+ current.each do |constant|
+ values.push(verify_constant(modul, constant, nil))
+ end
+ values.delete_if { |value| value.nil? }
+ end
+
+ values
+ end
+
+ # check that `current` (string or integer) is a contant in `modul`.
+ def verify_constant(modul, current, default)
+ constant = modul.constants.find { |name| modul.const_get(name) == current }
+ value = constant.nil? ? default : modul.const_get(constant)
+ if current.is_a? String
+ value = modul.const_get(current.upcase) rescue default
+ end
+
+ value
+ end
+
+ def absolute(path)
+ File.expand_path(path, Rails.root)
+ end
+
+ private
+
+ def base_url(config)
+ custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
+
+ [
+ config.protocol,
+ "://",
+ config.host,
+ custom_port
+ ]
+ end
+
+ def on_standard_port?(config)
+ config.port.to_i == (config.https ? 443 : 80)
+ end
+
+ # Extract the host part of the given +url+.
+ def host(url)
+ url = url.downcase
+ url = "http://#{url}" unless url.start_with?('http')
+
+ # Get rid of the path so that we don't even have to encode it
+ url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
+
+ URI.parse(url_without_path).host
+ end
+
+ # Runs every minute in a random ten-minute period on Sundays, to balance the
+ # load on the server receiving these pings. The usage ping is safe to run
+ # multiple times because of a 24 hour exclusive lock.
+ def cron_for_usage_ping
+ hour = rand(24)
+ minute = rand(6)
+
+ "#{minute}0-#{minute}9 #{hour} * * 0"
+ end
+ end
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 47fbbed44cf..d16060e8f45 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -73,3 +73,7 @@
- [object_storage, 1]
- [plugin, 1]
- [pipeline_background, 1]
+ - [repository_update_remote_mirror, 1]
+ - [repository_remove_remote, 1]
+ - [create_note_diff_file, 1]
+
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 39e9fbbd530..d6ab32972fb 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,30 +1,34 @@
-const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
+const VueLoaderPlugin = require('vue-loader/lib/plugin');
const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
-const NameAllModulesPlugin = require('name-all-modules-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
-const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ROOT_PATH = path.resolve(__dirname, '..');
+const CACHE_PATH = path.join(ROOT_PATH, 'tmp/cache');
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
-const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
+const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
const NO_COMPRESSION = process.env.NO_COMPRESSION;
+const VUE_VERSION = require('vue/package.json').version;
+const VUE_LOADER_VERSION = require('vue-loader/package.json').version;
+
let autoEntriesCount = 0;
let watchAutoEntries = [];
+const defaultEntries = ['./main'];
function generateEntries() {
// generate automatic entry points
const autoEntries = {};
+ const autoEntriesMap = {};
const pageEntries = glob.sync('pages/**/index.js', {
cwd: path.join(ROOT_PATH, 'app/assets/javascripts'),
});
@@ -33,25 +37,38 @@ function generateEntries() {
function generateAutoEntries(path, prefix = '.') {
const chunkPath = path.replace(/\/index\.js$/, '');
const chunkName = chunkPath.replace(/\//g, '.');
- autoEntries[chunkName] = `${prefix}/${path}`;
+ autoEntriesMap[chunkName] = `${prefix}/${path}`;
}
pageEntries.forEach(path => generateAutoEntries(path));
- autoEntriesCount = Object.keys(autoEntries).length;
+ const autoEntryKeys = Object.keys(autoEntriesMap);
+ autoEntriesCount = autoEntryKeys.length;
+
+ // import ancestor entrypoints within their children
+ autoEntryKeys.forEach(entry => {
+ const entryPaths = [autoEntriesMap[entry]];
+ const segments = entry.split('.');
+ while (segments.pop()) {
+ const ancestor = segments.join('.');
+ if (autoEntryKeys.includes(ancestor)) {
+ entryPaths.unshift(autoEntriesMap[ancestor]);
+ }
+ }
+ autoEntries[entry] = defaultEntries.concat(entryPaths);
+ });
const manualEntries = {
- common: './commons/index.js',
- main: './main.js',
+ default: defaultEntries,
raven: './raven/index.js',
- webpack_runtime: './webpack.js',
- ide: './ide/index.js',
};
return Object.assign(manualEntries, autoEntries);
}
-const config = {
+module.exports = {
+ mode: IS_PRODUCTION ? 'production' : 'development',
+
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: generateEntries,
@@ -59,20 +76,48 @@ const config = {
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
publicPath: '/assets/webpack/',
- filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js',
- chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js',
+ filename: IS_PRODUCTION ? '[name].[chunkhash:8].bundle.js' : '[name].bundle.js',
+ chunkFilename: IS_PRODUCTION ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js',
+ globalObject: 'this', // allow HMR and web workers to play nice
+ },
+
+ resolve: {
+ extensions: ['.js'],
+ alias: {
+ '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
+ emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
+ empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
+ icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
+ images: path.join(ROOT_PATH, 'app/assets/images'),
+ vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ vue$: 'vue/dist/vue.esm.js',
+ spec: path.join(ROOT_PATH, 'spec/javascripts'),
+ },
},
module: {
+ strictExportPresence: true,
rules: [
{
test: /\.js$/,
- exclude: /(node_modules|vendor\/assets)/,
+ exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path),
loader: 'babel-loader',
+ options: {
+ cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
+ },
},
{
test: /\.vue$/,
loader: 'vue-loader',
+ options: {
+ cacheDirectory: path.join(CACHE_PATH, 'vue-loader'),
+ cacheIdentifier: [
+ process.env.NODE_ENV || 'development',
+ webpack.version,
+ VUE_VERSION,
+ VUE_LOADER_VERSION,
+ ].join('|'),
+ },
},
{
test: /\.svg$/,
@@ -89,10 +134,10 @@ const config = {
{
loader: 'worker-loader',
options: {
- inline: true,
+ name: '[name].[hash:8].worker.js',
},
},
- { loader: 'babel-loader' },
+ 'babel-loader',
],
},
{
@@ -100,18 +145,17 @@ const config = {
exclude: /node_modules/,
loader: 'file-loader',
options: {
- name: '[name].[hash].[ext]',
+ name: '[name].[hash:8].[ext]',
},
},
{
- test: /katex.min.css$/,
- include: /node_modules\/katex\/dist/,
+ test: /.css$/,
use: [
- { loader: 'style-loader' },
+ 'vue-style-loader',
{
loader: 'css-loader',
options: {
- name: '[name].[hash].[ext]',
+ name: '[name].[hash:8].[ext]',
},
},
],
@@ -121,7 +165,7 @@ const config = {
include: /node_modules\/katex\/dist\/fonts/,
loader: 'file-loader',
options: {
- name: '[name].[hash].[ext]',
+ name: '[name].[hash:8].[ext]',
},
},
{
@@ -132,9 +176,34 @@ const config = {
],
},
],
-
noParse: [/monaco-editor\/\w+\/vs\//],
- strictExportPresence: true,
+ },
+
+ optimization: {
+ nodeEnv: false,
+ runtimeChunk: 'single',
+ splitChunks: {
+ maxInitialRequests: 4,
+ cacheGroups: {
+ default: false,
+ common: () => ({
+ priority: 20,
+ name: 'main',
+ chunks: 'initial',
+ minChunks: autoEntriesCount * 0.9,
+ }),
+ vendors: {
+ priority: 10,
+ chunks: 'async',
+ test: /[\\/](node_modules|vendor[\\/]assets[\\/]javascripts)[\\/]/,
+ },
+ commons: {
+ chunks: 'all',
+ minChunks: 2,
+ reuseExistingChunk: true,
+ },
+ },
+ },
},
plugins: [
@@ -154,6 +223,9 @@ const config = {
},
}),
+ // enable vue-loader to use existing loader rules for other module types
+ new VueLoaderPlugin(),
+
// prevent pikaday from including moment.js
new webpack.IgnorePlugin(/moment/, /pikaday/),
@@ -163,54 +235,6 @@ const config = {
jQuery: 'jquery',
}),
- // assign deterministic module ids
- new webpack.NamedModulesPlugin(),
- new NameAllModulesPlugin(),
-
- // assign deterministic chunk ids
- new webpack.NamedChunksPlugin(chunk => {
- if (chunk.name) {
- return chunk.name;
- }
-
- const moduleNames = [];
-
- function collectModuleNames(m) {
- // handle ConcatenatedModule which does not have resource nor context set
- if (m.modules) {
- m.modules.forEach(collectModuleNames);
- return;
- }
-
- const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages');
-
- if (m.resource.indexOf(pagesBase) === 0) {
- moduleNames.push(
- path
- .relative(pagesBase, m.resource)
- .replace(/\/index\.[a-z]+$/, '')
- .replace(/\//g, '__')
- );
- } else {
- moduleNames.push(path.relative(m.context, m.resource));
- }
- }
-
- chunk.forEachModule(collectModuleNames);
-
- const hash = crypto
- .createHash('sha256')
- .update(moduleNames.join('_'))
- .digest('hex');
-
- return `${moduleNames[0]}-${hash.substr(0, 6)}`;
- }),
-
- // create cacheable common library bundles
- new webpack.optimize.CommonsChunkPlugin({
- names: ['main', 'common', 'webpack_runtime'],
- }),
-
// copy pre-compiled vendor libraries verbatim
new CopyWebpackPlugin([
{
@@ -233,74 +257,26 @@ const config = {
},
},
]),
- ],
- resolve: {
- extensions: ['.js'],
- alias: {
- '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
- emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
- empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
- icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
- images: path.join(ROOT_PATH, 'app/assets/images'),
- vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
- vue$: 'vue/dist/vue.esm.js',
- spec: path.join(ROOT_PATH, 'spec/javascripts'),
- },
- },
+ // compression can require a lot of compute time and is disabled in CI
+ IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(),
- // sqljs requires fs
- node: {
- fs: 'empty',
- },
-};
+ // WatchForChangesPlugin
+ // TODO: publish this as a separate plugin
+ IS_DEV_SERVER && {
+ apply(compiler) {
+ compiler.hooks.emit.tapAsync('WatchForChangesPlugin', (compilation, callback) => {
+ const missingDeps = Array.from(compilation.missingDependencies);
+ const nodeModulesPath = path.join(ROOT_PATH, 'node_modules');
+ const hasMissingNodeModules = missingDeps.some(
+ file => file.indexOf(nodeModulesPath) !== -1
+ );
-if (IS_PRODUCTION) {
- config.devtool = 'source-map';
- config.plugins.push(
- new webpack.NoEmitOnErrorsPlugin(),
- new webpack.LoaderOptionsPlugin({
- minimize: true,
- debug: false,
- }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.optimize.UglifyJsPlugin({
- sourceMap: true,
- }),
- new webpack.DefinePlugin({
- 'process.env': { NODE_ENV: JSON.stringify('production') },
- })
- );
-
- // compression can require a lot of compute time and is disabled in CI
- if (!NO_COMPRESSION) {
- config.plugins.push(new CompressionPlugin());
- }
-}
+ // watch for changes to missing node_modules
+ if (hasMissingNodeModules) compilation.contextDependencies.add(nodeModulesPath);
-if (IS_DEV_SERVER) {
- config.devtool = 'cheap-module-eval-source-map';
- config.devServer = {
- host: DEV_SERVER_HOST,
- port: DEV_SERVER_PORT,
- disableHostCheck: true,
- headers: { 'Access-Control-Allow-Origin': '*' },
- stats: 'errors-only',
- hot: DEV_SERVER_LIVERELOAD,
- inline: DEV_SERVER_LIVERELOAD,
- };
- config.plugins.push(
- // watch node_modules for changes if we encounter a missing module compile error
- new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')),
-
- // watch for changes to our automatic entry point modules
- {
- apply(compiler) {
- compiler.plugin('emit', (compilation, callback) => {
- compilation.contextDependencies = [
- ...compilation.contextDependencies,
- ...watchAutoEntries,
- ];
+ // watch for changes to automatic entrypoints
+ watchAutoEntries.forEach(watchPath => compilation.contextDependencies.add(watchPath));
// report our auto-generated bundle count
console.log(
@@ -310,23 +286,37 @@ if (IS_DEV_SERVER) {
callback();
});
},
- }
- );
- if (DEV_SERVER_LIVERELOAD) {
- config.plugins.push(new webpack.HotModuleReplacementPlugin());
- }
-}
+ },
-if (WEBPACK_REPORT) {
- config.plugins.push(
- new BundleAnalyzerPlugin({
- analyzerMode: 'static',
- generateStatsFile: true,
- openAnalyzer: false,
- reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
- statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
- })
- );
-}
+ // enable HMR only in webpack-dev-server
+ DEV_SERVER_LIVERELOAD && new webpack.HotModuleReplacementPlugin(),
+
+ // optionally generate webpack bundle analysis
+ WEBPACK_REPORT &&
+ new BundleAnalyzerPlugin({
+ analyzerMode: 'static',
+ generateStatsFile: true,
+ openAnalyzer: false,
+ reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
+ statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
+ }),
+ ].filter(Boolean),
+
+ devServer: {
+ host: DEV_SERVER_HOST,
+ port: DEV_SERVER_PORT,
+ disableHostCheck: true,
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Headers': '*',
+ },
+ stats: 'errors-only',
+ hot: DEV_SERVER_LIVERELOAD,
+ inline: DEV_SERVER_LIVERELOAD,
+ },
+
+ devtool: IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map',
-module.exports = config;
+ // sqljs requires fs
+ node: { fs: 'empty' },
+};
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index d7be6f5950f..7b9a4bad449 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -1,5 +1,5 @@
require './spec/support/sidekiq'
-require './spec/support/test_env'
+require './spec/support/helpers/test_env'
class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false)
diff --git a/db/migrate/20121220064453_init_schema.rb b/db/migrate/20121220064453_init_schema.rb
deleted file mode 100644
index f93dc92b70f..00000000000
--- a/db/migrate/20121220064453_init_schema.rb
+++ /dev/null
@@ -1,307 +0,0 @@
-# rubocop:disable all
-class InitSchema < ActiveRecord::Migration
- def up
-
- create_table "events", force: true do |t|
- t.string "target_type"
- t.integer "target_id"
- t.string "title"
- t.text "data"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "action"
- t.integer "author_id"
- end
-
- add_index "events", ["action"], name: "index_events_on_action", using: :btree
- add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
- add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
- add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree
- add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
- add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
-
- create_table "issues", force: true do |t|
- t.string "title"
- t.integer "assignee_id"
- t.integer "author_id"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "closed", default: false, null: false
- t.integer "position", default: 0
- t.string "branch_name"
- t.text "description"
- t.integer "milestone_id"
- end
-
- add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
- add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
- add_index "issues", ["closed"], name: "index_issues_on_closed", using: :btree
- add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
- add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
- add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
- add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
-
- create_table "keys", force: true do |t|
- t.integer "user_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.text "key"
- t.string "title"
- t.string "identifier"
- t.integer "project_id"
- end
-
- add_index "keys", ["identifier"], name: "index_keys_on_identifier", using: :btree
- add_index "keys", ["project_id"], name: "index_keys_on_project_id", using: :btree
- add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
-
- create_table "merge_requests", force: true do |t|
- t.string "target_branch", null: false
- t.string "source_branch", null: false
- t.integer "project_id", null: false
- t.integer "author_id"
- t.integer "assignee_id"
- t.string "title"
- t.boolean "closed", default: false, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.text "st_commits"
- t.text "st_diffs"
- t.boolean "merged", default: false, null: false
- t.integer "state", default: 1, null: false
- t.integer "milestone_id"
- 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", ["closed"], name: "index_merge_requests_on_closed", using: :btree
- add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
- add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
- add_index "merge_requests", ["project_id"], name: "index_merge_requests_on_project_id", using: :btree
- add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
- add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
- add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
-
- create_table "milestones", force: true do |t|
- t.string "title", null: false
- t.integer "project_id", null: false
- t.text "description"
- t.date "due_date"
- t.boolean "closed", default: false, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
- add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
-
- create_table "namespaces", force: true do |t|
- t.string "name", null: false
- t.string "path", null: false
- t.integer "owner_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "type"
- end
-
- add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree
- add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
- add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
- add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
-
- create_table "notes", force: true do |t|
- t.text "note"
- t.string "noteable_type"
- t.integer "author_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "project_id"
- t.string "attachment"
- t.string "line_code"
- t.string "commit_id"
- t.integer "noteable_id"
- end
-
- 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", ["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", ["project_id"], name: "index_notes_on_project_id", using: :btree
-
- create_table "projects", force: true do |t|
- t.string "name"
- t.string "path"
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "private_flag", default: true, null: false
- t.integer "owner_id"
- t.string "default_branch"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "wall_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
- t.integer "namespace_id"
- end
-
- add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
- add_index "projects", ["owner_id"], name: "index_projects_on_owner_id", using: :btree
-
- create_table "protected_branches", force: true do |t|
- t.integer "project_id", null: false
- t.string "name", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "services", force: true do |t|
- t.string "type"
- t.string "title"
- t.string "token"
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "active", default: false, null: false
- t.string "project_url"
- end
-
- add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
-
- create_table "snippets", force: true do |t|
- t.string "title"
- t.text "content"
- t.integer "author_id", null: false
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "file_name"
- t.datetime "expires_at"
- end
-
- add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
- add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
- add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
-
- create_table "taggings", force: true do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context"
- t.datetime "created_at"
- end
-
- 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
-
- create_table "tags", force: true do |t|
- t.string "name"
- end
-
- create_table "user_team_project_relationships", force: true do |t|
- t.integer "project_id"
- t.integer "user_team_id"
- t.integer "greatest_access"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "user_team_user_relationships", force: true do |t|
- t.integer "user_id"
- t.integer "user_team_id"
- t.boolean "group_admin"
- t.integer "permission"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "user_teams", force: true do |t|
- t.string "name"
- t.string "path"
- t.integer "owner_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "users", force: true do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
- t.datetime "reset_password_sent_at"
- t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0
- t.datetime "current_sign_in_at"
- t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
- t.string "last_sign_in_ip"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "name"
- t.boolean "admin", default: false, null: false
- t.integer "projects_limit", default: 10
- t.string "skype", default: "", null: false
- t.string "linkedin", default: "", null: false
- t.string "twitter", default: "", null: false
- t.string "authentication_token"
- t.boolean "dark_scheme", default: false, null: false
- t.integer "theme_id", default: 1, null: false
- t.string "bio"
- t.boolean "blocked", default: false, null: false
- t.integer "failed_attempts", default: 0
- t.datetime "locked_at"
- t.string "extern_uid"
- t.string "provider"
- t.string "username"
- end
-
- add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
- add_index "users", ["blocked"], name: "index_users_on_blocked", using: :btree
- add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
- add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree
- add_index "users", ["name"], name: "index_users_on_name", using: :btree
- add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
- add_index "users", ["username"], name: "index_users_on_username", using: :btree
-
- create_table "users_projects", force: true do |t|
- t.integer "user_id", null: false
- t.integer "project_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "project_access", default: 0, null: false
- end
-
- add_index "users_projects", ["project_access"], name: "index_users_projects_on_project_access", using: :btree
- add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree
- add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree
-
- create_table "web_hooks", force: true do |t|
- t.string "url"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
- t.integer "service_id"
- end
-
- create_table "wikis", force: true do |t|
- t.string "title"
- t.text "content"
- t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "slug"
- t.integer "user_id"
- end
-
- add_index "wikis", ["project_id"], name: "index_wikis_on_project_id", using: :btree
- add_index "wikis", ["slug"], name: "index_wikis_on_slug", using: :btree
-
- end
-
- def down
- raise "Can not revert initial migration"
- end
-end
diff --git a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb
deleted file mode 100644
index 84fd2060770..00000000000
--- a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class RenameOwnerToCreatorForProject < ActiveRecord::Migration
- def change
- rename_column :projects, :owner_id, :creator_id
- end
-end
diff --git a/db/migrate/20130110172407_add_public_to_project.rb b/db/migrate/20130110172407_add_public_to_project.rb
deleted file mode 100644
index 4362aadcc1d..00000000000
--- a/db/migrate/20130110172407_add_public_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPublicToProject < ActiveRecord::Migration
- def change
- add_column :projects, :public, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20130123114545_add_issues_tracker_to_project.rb b/db/migrate/20130123114545_add_issues_tracker_to_project.rb
deleted file mode 100644
index ba8c50b53e2..00000000000
--- a/db/migrate/20130123114545_add_issues_tracker_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIssuesTrackerToProject < ActiveRecord::Migration
- def change
- add_column :projects, :issues_tracker, :string, default: :gitlab, null: false
- end
-end
diff --git a/db/migrate/20130125090214_add_user_permissions.rb b/db/migrate/20130125090214_add_user_permissions.rb
deleted file mode 100644
index 1350eadb60e..00000000000
--- a/db/migrate/20130125090214_add_user_permissions.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# rubocop:disable all
-class AddUserPermissions < ActiveRecord::Migration
- def up
- add_column :users, :can_create_group, :boolean, default: true, null: false
- add_column :users, :can_create_team, :boolean, default: true, null: false
- end
-
- def down
- remove_column :users, :can_create_group
- remove_column :users, :can_create_team
- end
-end
diff --git a/db/migrate/20130131070232_remove_private_flag_from_project.rb b/db/migrate/20130131070232_remove_private_flag_from_project.rb
deleted file mode 100644
index f0273ba448e..00000000000
--- a/db/migrate/20130131070232_remove_private_flag_from_project.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemovePrivateFlagFromProject < ActiveRecord::Migration
- def up
- remove_column :projects, :private_flag
- end
-
- def down
- add_column :projects, :private_flag, :boolean, default: true, null: false
- end
-end
diff --git a/db/migrate/20130206084024_add_description_to_namsespace.rb b/db/migrate/20130206084024_add_description_to_namsespace.rb
deleted file mode 100644
index 62676ce8914..00000000000
--- a/db/migrate/20130206084024_add_description_to_namsespace.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddDescriptionToNamsespace < ActiveRecord::Migration
- def change
- add_column :namespaces, :description, :string, default: '', null: false
- end
-end
diff --git a/db/migrate/20130207104426_add_description_to_teams.rb b/db/migrate/20130207104426_add_description_to_teams.rb
deleted file mode 100644
index bd9a4767b69..00000000000
--- a/db/migrate/20130207104426_add_description_to_teams.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddDescriptionToTeams < ActiveRecord::Migration
- def change
- add_column :user_teams, :description, :string, default: '', null: false
- end
-end
diff --git a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb
deleted file mode 100644
index 56b01cbf892..00000000000
--- a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIssuesTrackerIdToProject < ActiveRecord::Migration
- def change
- add_column :projects, :issues_tracker_id, :string
- end
-end
diff --git a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb
deleted file mode 100644
index 4722cc13d4b..00000000000
--- a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class RenameStateToMergeStatusInMilestone < ActiveRecord::Migration
- def change
- rename_column :merge_requests, :state, :merge_status
- end
-end
diff --git a/db/migrate/20130218140952_add_state_to_issue.rb b/db/migrate/20130218140952_add_state_to_issue.rb
deleted file mode 100644
index 3a5e978a182..00000000000
--- a/db/migrate/20130218140952_add_state_to_issue.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToIssue < ActiveRecord::Migration
- def change
- add_column :issues, :state, :string
- end
-end
diff --git a/db/migrate/20130218141038_add_state_to_merge_request.rb b/db/migrate/20130218141038_add_state_to_merge_request.rb
deleted file mode 100644
index e0180c755e2..00000000000
--- a/db/migrate/20130218141038_add_state_to_merge_request.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToMergeRequest < ActiveRecord::Migration
- def change
- add_column :merge_requests, :state, :string
- end
-end
diff --git a/db/migrate/20130218141117_add_state_to_milestone.rb b/db/migrate/20130218141117_add_state_to_milestone.rb
deleted file mode 100644
index 5f71608692c..00000000000
--- a/db/migrate/20130218141117_add_state_to_milestone.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToMilestone < ActiveRecord::Migration
- def change
- add_column :milestones, :state, :string
- end
-end
diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
deleted file mode 100644
index 67a0d3b53eb..00000000000
--- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# rubocop:disable all
-class ConvertClosedToStateInIssue < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
- execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
- end
-
- def down
- execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
- end
-
- private
-
- def table_name
- Issue.table_name
- end
-end
diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
deleted file mode 100644
index 307fc6a023d..00000000000
--- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# rubocop:disable all
-class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}"
- execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value} AND merged = #{false_value}"
- execute "UPDATE #{table_name} SET state = 'opened' WHERE closed = #{false_value}"
- end
-
- def down
- execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'closed'"
- execute "UPDATE #{table_name} SET closed = #{true_value}, merged = #{true_value} WHERE state = 'merged'"
- end
-
- private
-
- def table_name
- MergeRequest.table_name
- end
-end
diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
deleted file mode 100644
index d12703cf3b2..00000000000
--- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# rubocop:disable all
-class ConvertClosedToStateInMilestone < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
- execute "UPDATE #{table_name} SET state = 'active' WHERE closed = #{false_value}"
- end
-
- def down
- execute "UPDATE #{table_name} SET closed = #{true_value} WHERE state = 'cloesd'"
- end
-
- private
-
- def table_name
- Milestone.table_name
- end
-end
diff --git a/db/migrate/20130218141444_remove_merged_from_merge_request.rb b/db/migrate/20130218141444_remove_merged_from_merge_request.rb
deleted file mode 100644
index afa5137061e..00000000000
--- a/db/migrate/20130218141444_remove_merged_from_merge_request.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveMergedFromMergeRequest < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :merged
- end
-
- def down
- add_column :merge_requests, :merged, :boolean, default: true, null: false
- end
-end
diff --git a/db/migrate/20130218141507_remove_closed_from_issue.rb b/db/migrate/20130218141507_remove_closed_from_issue.rb
deleted file mode 100644
index f250288bc3b..00000000000
--- a/db/migrate/20130218141507_remove_closed_from_issue.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveClosedFromIssue < ActiveRecord::Migration
- def up
- remove_column :issues, :closed
- end
-
- def down
- add_column :issues, :closed, :boolean
- end
-end
diff --git a/db/migrate/20130218141536_remove_closed_from_merge_request.rb b/db/migrate/20130218141536_remove_closed_from_merge_request.rb
deleted file mode 100644
index efa12e32636..00000000000
--- a/db/migrate/20130218141536_remove_closed_from_merge_request.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveClosedFromMergeRequest < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :closed
- end
-
- def down
- add_column :merge_requests, :closed, :boolean
- end
-end
diff --git a/db/migrate/20130218141554_remove_closed_from_milestone.rb b/db/migrate/20130218141554_remove_closed_from_milestone.rb
deleted file mode 100644
index 75ac14e43be..00000000000
--- a/db/migrate/20130218141554_remove_closed_from_milestone.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveClosedFromMilestone < ActiveRecord::Migration
- def up
- remove_column :milestones, :closed
- end
-
- def down
- add_column :milestones, :closed, :boolean
- end
-end
diff --git a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb
deleted file mode 100644
index 97615e47c89..00000000000
--- a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNewMergeStatusToMergeRequest < ActiveRecord::Migration
- def change
- add_column :merge_requests, :new_merge_status, :string
- end
-end
diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
deleted file mode 100644
index 3b8c3686c55..00000000000
--- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# rubocop:disable all
-class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration
- def up
- execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1"
- execute "UPDATE #{table_name} SET new_merge_status = 'can_be_merged' WHERE merge_status = 2"
- execute "UPDATE #{table_name} SET new_merge_status = 'cannot_be_merged' WHERE merge_status = 3"
- end
-
- def down
- execute "UPDATE #{table_name} SET merge_status = 1 WHERE new_merge_status = 'unchecked'"
- execute "UPDATE #{table_name} SET merge_status = 2 WHERE new_merge_status = 'can_be_merged'"
- execute "UPDATE #{table_name} SET merge_status = 3 WHERE new_merge_status = 'cannot_be_merged'"
- end
-
- private
-
- def table_name
- MergeRequest.table_name
- end
-end
diff --git a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb
deleted file mode 100644
index bd25ffbfc99..00000000000
--- a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveMergeStatusFromMergeRequest < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :merge_status
- end
-
- def down
- add_column :merge_requests, :merge_status, :integer
- end
-end
diff --git a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb
deleted file mode 100644
index f0595720a39..00000000000
--- a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class RenameNewMergeStatusToMergeStatusInMilestone < ActiveRecord::Migration
- def change
- rename_column :merge_requests, :new_merge_status, :merge_status
- end
-end
diff --git a/db/migrate/20130304104623_add_state_to_user.rb b/db/migrate/20130304104623_add_state_to_user.rb
deleted file mode 100644
index 4456d022e3f..00000000000
--- a/db/migrate/20130304104623_add_state_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStateToUser < ActiveRecord::Migration
- def change
- add_column :users, :state, :string
- end
-end
diff --git a/db/migrate/20130304104740_convert_blocked_to_state.rb b/db/migrate/20130304104740_convert_blocked_to_state.rb
deleted file mode 100644
index 9afd1093645..00000000000
--- a/db/migrate/20130304104740_convert_blocked_to_state.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class ConvertBlockedToState < ActiveRecord::Migration
- def up
- User.transaction do
- User.where(blocked: true).update_all(state: :blocked)
- User.where(blocked: false).update_all(state: :active)
- end
- end
-
- def down
- User.transaction do
- User.where(state: :blocked).update_all(blocked: :true)
- end
- end
-end
diff --git a/db/migrate/20130304105317_remove_blocked_from_user.rb b/db/migrate/20130304105317_remove_blocked_from_user.rb
deleted file mode 100644
index 8f5b2c59b43..00000000000
--- a/db/migrate/20130304105317_remove_blocked_from_user.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveBlockedFromUser < ActiveRecord::Migration
- def up
- remove_column :users, :blocked
- end
-
- def down
- add_column :users, :blocked, :boolean
- end
-end
diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb
deleted file mode 100644
index 09af928fde7..00000000000
--- a/db/migrate/20130315124931_user_color_scheme.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class UserColorScheme < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- add_column :users, :color_scheme_id, :integer, null: false, default: 1
- execute("UPDATE users SET color_scheme_id = 2 WHERE dark_scheme = #{true_value}")
- remove_column :users, :dark_scheme
- end
-
- def down
- add_column :users, :dark_scheme, :boolean, null: false, default: false
- remove_column :users, :color_scheme_id
- end
-end
diff --git a/db/migrate/20130318212250_add_snippets_to_features.rb b/db/migrate/20130318212250_add_snippets_to_features.rb
deleted file mode 100644
index 9860b85f504..00000000000
--- a/db/migrate/20130318212250_add_snippets_to_features.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddSnippetsToFeatures < ActiveRecord::Migration
- def change
- add_column :projects, :snippets_enabled, :boolean, null: false, default: true
- end
-end
diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb
deleted file mode 100644
index 065a5e08243..00000000000
--- a/db/migrate/20130319214458_create_forked_project_links.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# rubocop:disable all
-class CreateForkedProjectLinks < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :forked_project_links do |t|
- t.integer :forked_to_project_id, null: false
- t.integer :forked_from_project_id, null: false
-
- t.timestamps null: true
- end
- add_index :forked_project_links, :forked_to_project_id, unique: true
- end
-end
diff --git a/db/migrate/20130323174317_add_private_to_snippets.rb b/db/migrate/20130323174317_add_private_to_snippets.rb
deleted file mode 100644
index 376f4618d41..00000000000
--- a/db/migrate/20130323174317_add_private_to_snippets.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPrivateToSnippets < ActiveRecord::Migration
- def change
- add_column :snippets, :private, :boolean, null: false, default: true
- end
-end
diff --git a/db/migrate/20130324151736_add_type_to_snippets.rb b/db/migrate/20130324151736_add_type_to_snippets.rb
deleted file mode 100644
index 097cb9bc7cb..00000000000
--- a/db/migrate/20130324151736_add_type_to_snippets.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddTypeToSnippets < ActiveRecord::Migration
- def change
- add_column :snippets, :type, :string
- end
-end
diff --git a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb
deleted file mode 100644
index 9256e62086e..00000000000
--- a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration
- def up
- change_column :snippets, :project_id, :integer, :null => true
- end
-
- def down
- change_column :snippets, :project_id, :integer, :null => false
- end
-end
diff --git a/db/migrate/20130324203535_add_type_value_for_snippets.rb b/db/migrate/20130324203535_add_type_value_for_snippets.rb
deleted file mode 100644
index 6e910fd74c7..00000000000
--- a/db/migrate/20130324203535_add_type_value_for_snippets.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# rubocop:disable all
-class AddTypeValueForSnippets < ActiveRecord::Migration
- def up
- Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet')
- end
-
- def down
- end
-end
diff --git a/db/migrate/20130325173941_add_notification_level_to_user.rb b/db/migrate/20130325173941_add_notification_level_to_user.rb
deleted file mode 100644
index 1dc58d4bcc8..00000000000
--- a/db/migrate/20130325173941_add_notification_level_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNotificationLevelToUser < ActiveRecord::Migration
- def change
- add_column :users, :notification_level, :integer, null: false, default: 1
- end
-end
diff --git a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb
deleted file mode 100644
index 0592181927e..00000000000
--- a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIndexToUsersAuthenticationToken < ActiveRecord::Migration
- def change
- add_index :users, :authentication_token, unique: true
- end
-end
diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb
deleted file mode 100644
index 04a01612c6f..00000000000
--- a/db/migrate/20130403003950_add_last_activity_column_into_project.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# rubocop:disable all
-class AddLastActivityColumnIntoProject < ActiveRecord::Migration
- def up
- add_column :projects, :last_activity_at, :datetime
- add_index :projects, :last_activity_at
-
- select_all('SELECT id, updated_at FROM projects').each do |project|
- project_id = project['id']
- update_date = project['updated_at']
- event = select_one("SELECT created_at FROM events WHERE project_id = #{project_id} ORDER BY created_at DESC LIMIT 1")
-
- if event && event['created_at']
- update_date = event['created_at']
- end
-
- execute("UPDATE projects SET last_activity_at = '#{update_date}' WHERE id = #{project_id}")
- end
- end
-
- def down
- remove_index :projects, :last_activity_at
- remove_column :projects, :last_activity_at
- end
-end
diff --git a/db/migrate/20130404164628_add_notification_level_to_user_project.rb b/db/migrate/20130404164628_add_notification_level_to_user_project.rb
deleted file mode 100644
index 1e072d9c6e1..00000000000
--- a/db/migrate/20130404164628_add_notification_level_to_user_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNotificationLevelToUserProject < ActiveRecord::Migration
- def change
- add_column :users_projects, :notification_level, :integer, null: false, default: 3
- end
-end
diff --git a/db/migrate/20130410175022_remove_wiki_table.rb b/db/migrate/20130410175022_remove_wiki_table.rb
deleted file mode 100644
index 5885b1cc375..00000000000
--- a/db/migrate/20130410175022_remove_wiki_table.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveWikiTable < ActiveRecord::Migration
- def up
- drop_table :wikis
- end
-
- def down
- raise ActiveRecord::IrreversibleMigration
- end
-end
diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb
deleted file mode 100644
index ec953986c6a..00000000000
--- a/db/migrate/20130419190306_allow_merges_for_forks.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# rubocop:disable all
-class AllowMergesForForks < ActiveRecord::Migration
- def self.up
- add_column :merge_requests, :target_project_id, :integer, :null => true
- execute "UPDATE #{table_name} SET target_project_id = project_id"
- change_column :merge_requests, :target_project_id, :integer, :null => false
- rename_column :merge_requests, :project_id, :source_project_id
- end
-
- def self.down
- remove_column :merge_requests, :target_project_id
- rename_column :merge_requests, :source_project_id,:project_id
- end
-
- private
-
- def table_name
- MergeRequest.table_name
- end
-end
diff --git a/db/migrate/20130506085413_add_type_to_key.rb b/db/migrate/20130506085413_add_type_to_key.rb
deleted file mode 100644
index c9f1ee4e389..00000000000
--- a/db/migrate/20130506085413_add_type_to_key.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddTypeToKey < ActiveRecord::Migration
- def change
- add_column :keys, :type, :string
- end
-end
diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb
deleted file mode 100644
index 8b9662a27c3..00000000000
--- a/db/migrate/20130506090604_create_deploy_keys_projects.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# rubocop:disable all
-class CreateDeployKeysProjects < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :deploy_keys_projects do |t|
- t.integer :deploy_key_id, null: false
- t.integer :project_id, null: false
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20130506095501_remove_project_id_from_key.rb b/db/migrate/20130506095501_remove_project_id_from_key.rb
deleted file mode 100644
index 53abc4e7b52..00000000000
--- a/db/migrate/20130506095501_remove_project_id_from_key.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# rubocop:disable all
-class RemoveProjectIdFromKey < ActiveRecord::Migration
- def up
- puts 'Migrate deploy keys: '
- Key.where('project_id IS NOT NULL').update_all(type: 'DeployKey')
-
- DeployKey.all.each do |key|
- project = Project.find_by(id: key.project_id)
- if project
- project.deploy_keys << key
- print '.'
- end
- end
-
- puts 'Done'
-
- remove_column :keys, :project_id
- end
-
- def down
- add_column :keys, :project_id, :integer
- end
-end
diff --git a/db/migrate/20130522141856_add_more_fields_to_service.rb b/db/migrate/20130522141856_add_more_fields_to_service.rb
deleted file mode 100644
index 9f764a1d050..00000000000
--- a/db/migrate/20130522141856_add_more_fields_to_service.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddMoreFieldsToService < ActiveRecord::Migration
- def change
- add_column :services, :subdomain, :string
- add_column :services, :room, :string
- end
-end
diff --git a/db/migrate/20130528184641_add_system_to_notes.rb b/db/migrate/20130528184641_add_system_to_notes.rb
deleted file mode 100644
index 27fbf8983ac..00000000000
--- a/db/migrate/20130528184641_add_system_to_notes.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# rubocop:disable all
-class AddSystemToNotes < ActiveRecord::Migration
- class Note < ActiveRecord::Base
- end
-
- def up
- add_column :notes, :system, :boolean, default: false, null: false
-
- Note.reset_column_information
- Note.update_all(system: false)
- Note.where("note like '_status changed to%'").update_all(system: true)
- end
-
- def down
- remove_column :notes, :system
- end
-end
diff --git a/db/migrate/20130611210815_increase_snippet_text_column_size.rb b/db/migrate/20130611210815_increase_snippet_text_column_size.rb
deleted file mode 100644
index f710c79a9a5..00000000000
--- a/db/migrate/20130611210815_increase_snippet_text_column_size.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class IncreaseSnippetTextColumnSize < ActiveRecord::Migration
- def up
- # MYSQL LARGETEXT for snippet
- change_column :snippets, :content, :text, :limit => 4294967295
- end
-
- def down
- end
-end
diff --git a/db/migrate/20130613165816_add_password_expires_at_to_users.rb b/db/migrate/20130613165816_add_password_expires_at_to_users.rb
deleted file mode 100644
index 47306a370a8..00000000000
--- a/db/migrate/20130613165816_add_password_expires_at_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPasswordExpiresAtToUsers < ActiveRecord::Migration
- def change
- add_column :users, :password_expires_at, :datetime
- end
-end
diff --git a/db/migrate/20130613173246_add_created_by_id_to_user.rb b/db/migrate/20130613173246_add_created_by_id_to_user.rb
deleted file mode 100644
index 3138c0f40a7..00000000000
--- a/db/migrate/20130613173246_add_created_by_id_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddCreatedByIdToUser < ActiveRecord::Migration
- def change
- add_column :users, :created_by_id, :integer
- end
-end
diff --git a/db/migrate/20130614132337_add_improted_to_project.rb b/db/migrate/20130614132337_add_improted_to_project.rb
deleted file mode 100644
index 26dc16e3b43..00000000000
--- a/db/migrate/20130614132337_add_improted_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddImprotedToProject < ActiveRecord::Migration
- def change
- add_column :projects, :imported, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb
deleted file mode 100644
index 4ba7d0c9461..00000000000
--- a/db/migrate/20130617095603_create_users_groups.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# rubocop:disable all
-class CreateUsersGroups < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :users_groups do |t|
- t.integer :group_access, null: false
- t.integer :group_id, null: false
- t.integer :user_id, null: false
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20130621195223_add_notification_level_to_user_group.rb b/db/migrate/20130621195223_add_notification_level_to_user_group.rb
deleted file mode 100644
index 6fd4941f615..00000000000
--- a/db/migrate/20130621195223_add_notification_level_to_user_group.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddNotificationLevelToUserGroup < ActiveRecord::Migration
- def change
- add_column :users_groups, :notification_level, :integer, null: false, default: 3
- end
-end
diff --git a/db/migrate/20130622115340_add_more_db_index.rb b/db/migrate/20130622115340_add_more_db_index.rb
deleted file mode 100644
index 4113217de59..00000000000
--- a/db/migrate/20130622115340_add_more_db_index.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# rubocop:disable all
-class AddMoreDbIndex < ActiveRecord::Migration
- def change
- add_index :deploy_keys_projects, :project_id
- add_index :web_hooks, :project_id
- add_index :protected_branches, :project_id
-
- add_index :users_groups, :user_id
- add_index :snippets, :author_id
- add_index :notes, :author_id
- add_index :notes, [:noteable_id, :noteable_type]
- end
-end
diff --git a/db/migrate/20130624162710_add_fingerprint_to_key.rb b/db/migrate/20130624162710_add_fingerprint_to_key.rb
deleted file mode 100644
index 3e574ea81b9..00000000000
--- a/db/migrate/20130624162710_add_fingerprint_to_key.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddFingerprintToKey < ActiveRecord::Migration
- def change
- add_column :keys, :fingerprint, :string
- remove_column :keys, :identifier
- end
-end
diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb
deleted file mode 100644
index efccb2aa938..00000000000
--- a/db/migrate/20130711063759_create_project_group_links.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# rubocop:disable all
-class CreateProjectGroupLinks < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :project_group_links do |t|
- t.integer :project_id, null: false
- t.integer :group_id, null: false
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20130804151314_add_st_diff_to_note.rb b/db/migrate/20130804151314_add_st_diff_to_note.rb
deleted file mode 100644
index 9e2da73b695..00000000000
--- a/db/migrate/20130804151314_add_st_diff_to_note.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddStDiffToNote < ActiveRecord::Migration
- def change
- add_column :notes, :st_diff, :text, :null => true
- end
-end
diff --git a/db/migrate/20130809124851_add_permission_check_to_user.rb b/db/migrate/20130809124851_add_permission_check_to_user.rb
deleted file mode 100644
index 9f9dea36101..00000000000
--- a/db/migrate/20130809124851_add_permission_check_to_user.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddPermissionCheckToUser < ActiveRecord::Migration
- def change
- add_column :users, :last_credential_check_at, :datetime
- end
-end
diff --git a/db/migrate/20130812143708_add_import_url_to_project.rb b/db/migrate/20130812143708_add_import_url_to_project.rb
deleted file mode 100644
index d2bdfe1894e..00000000000
--- a/db/migrate/20130812143708_add_import_url_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddImportUrlToProject < ActiveRecord::Migration
- def change
- add_column :projects, :import_url, :string
- end
-end
diff --git a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb
deleted file mode 100644
index 0e0e78b0f0d..00000000000
--- a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddInternalIdsToIssuesAndMr < ActiveRecord::Migration
- def change
- add_column :issues, :iid, :integer
- add_column :merge_requests, :iid, :integer
- end
-end
diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb
deleted file mode 100644
index 98f3fa87523..00000000000
--- a/db/migrate/20130820102832_add_access_to_project_group_link.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddAccessToProjectGroupLink < ActiveRecord::Migration
- def change
- add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access
- end
-end
diff --git a/db/migrate/20130821090530_remove_deprecated_tables.rb b/db/migrate/20130821090530_remove_deprecated_tables.rb
deleted file mode 100644
index d22e713a7a1..00000000000
--- a/db/migrate/20130821090530_remove_deprecated_tables.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# rubocop:disable all
-class RemoveDeprecatedTables < ActiveRecord::Migration
- def up
- drop_table :user_teams
- drop_table :user_team_project_relationships
- drop_table :user_team_user_relationships
- end
-
- def down
- raise 'No rollback for this migration'
- end
-end
diff --git a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb
deleted file mode 100644
index e25b8f91662..00000000000
--- a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddInternalIdsToMilestones < ActiveRecord::Migration
- def change
- add_column :milestones, :iid, :integer
- end
-end
diff --git a/db/migrate/20130909132950_add_description_to_merge_request.rb b/db/migrate/20130909132950_add_description_to_merge_request.rb
deleted file mode 100644
index fbac50c8216..00000000000
--- a/db/migrate/20130909132950_add_description_to_merge_request.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddDescriptionToMergeRequest < ActiveRecord::Migration
- def change
- add_column :merge_requests, :description, :text, null: true
- end
-end
diff --git a/db/migrate/20130926081215_change_owner_id_for_group.rb b/db/migrate/20130926081215_change_owner_id_for_group.rb
deleted file mode 100644
index 2bdd22d5a04..00000000000
--- a/db/migrate/20130926081215_change_owner_id_for_group.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class ChangeOwnerIdForGroup < ActiveRecord::Migration
- def up
- change_column :namespaces, :owner_id, :integer, null: true
- end
-
- def down
- change_column :namespaces, :owner_id, :integer, null: false
- end
-end
diff --git a/db/migrate/20131005191208_add_avatar_to_users.rb b/db/migrate/20131005191208_add_avatar_to_users.rb
deleted file mode 100644
index df9057b81d6..00000000000
--- a/db/migrate/20131005191208_add_avatar_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddAvatarToUsers < ActiveRecord::Migration
- def change
- add_column :users, :avatar, :string
- end
-end
diff --git a/db/migrate/20131009115346_add_confirmable_to_users.rb b/db/migrate/20131009115346_add_confirmable_to_users.rb
deleted file mode 100644
index d714dd98e85..00000000000
--- a/db/migrate/20131009115346_add_confirmable_to_users.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# rubocop:disable all
-class AddConfirmableToUsers < ActiveRecord::Migration
- def self.up
- add_column :users, :confirmation_token, :string
- add_column :users, :confirmed_at, :datetime
- add_column :users, :confirmation_sent_at, :datetime
- add_column :users, :unconfirmed_email, :string
- add_index :users, :confirmation_token, unique: true
- User.update_all(confirmed_at: Time.now)
- end
-
- def self.down
- remove_column :users, :confirmation_token, :confirmed_at, :confirmation_sent_at
- remove_column :users, :unconfirmed_email
- end
-end
diff --git a/db/migrate/20131106151520_remove_default_branch.rb b/db/migrate/20131106151520_remove_default_branch.rb
deleted file mode 100644
index fd3d1ed7ab3..00000000000
--- a/db/migrate/20131106151520_remove_default_branch.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class RemoveDefaultBranch < ActiveRecord::Migration
- def up
- remove_column :projects, :default_branch
- end
-
- def down
- add_column :projects, :default_branch, :string
- end
-end
diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb
deleted file mode 100644
index ad2549e53af..00000000000
--- a/db/migrate/20131112114325_create_broadcast_messages.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class CreateBroadcastMessages < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :broadcast_messages do |t|
- t.text :message, null: false
- t.datetime :starts_at
- t.datetime :ends_at
- t.integer :alert_type
-
- t.timestamps null: true
- end
- end
-end
diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
deleted file mode 100644
index 86d73753adc..00000000000
--- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# rubocop:disable all
-class AddVisibilityLevelToProjects < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def self.up
- add_column :projects, :visibility_level, :integer, :default => 0, :null => false
- execute("UPDATE projects SET visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} WHERE public = #{true_value}")
- remove_column :projects, :public
- end
-
- def self.down
- add_column :projects, :public, :boolean, :default => false, :null => false
- execute("UPDATE projects SET public = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}")
- remove_column :projects, :visibility_level
- end
-end
diff --git a/db/migrate/20131129154016_add_archived_to_projects.rb b/db/migrate/20131129154016_add_archived_to_projects.rb
deleted file mode 100644
index e8e6908d137..00000000000
--- a/db/migrate/20131129154016_add_archived_to_projects.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddArchivedToProjects < ActiveRecord::Migration
- def change
- add_column :projects, :archived, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb
deleted file mode 100644
index 348a284a53e..00000000000
--- a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# rubocop:disable all
-class AddColorAndFontToBroadcastMessages < ActiveRecord::Migration
- def change
- add_column :broadcast_messages, :color, :string
- add_column :broadcast_messages, :font, :string
- end
-end
diff --git a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb
deleted file mode 100644
index 99d76611524..00000000000
--- a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# rubocop:disable all
-class AddEventFieldsForWebHook < ActiveRecord::Migration
- def change
- add_column :web_hooks, :push_events, :boolean, default: true, null: false
- add_column :web_hooks, :issues_events, :boolean, default: false, null: false
- add_column :web_hooks, :merge_requests_events, :boolean, default: false, null: false
- end
-end
diff --git a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb
deleted file mode 100644
index 4333dc59323..00000000000
--- a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddHideNoSshKeyToUsers < ActiveRecord::Migration
- def change
- add_column :users, :hide_no_ssh_key, :boolean, :default => false
- end
-end
diff --git a/db/migrate/20131217102743_add_recipients_to_service.rb b/db/migrate/20131217102743_add_recipients_to_service.rb
deleted file mode 100644
index 3c76be0f68d..00000000000
--- a/db/migrate/20131217102743_add_recipients_to_service.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddRecipientsToService < ActiveRecord::Migration
- def change
- add_column :services, :recipients, :text
- end
-end
diff --git a/db/migrate/20140116231608_add_website_url_to_users.rb b/db/migrate/20140116231608_add_website_url_to_users.rb
deleted file mode 100644
index 1c39423562e..00000000000
--- a/db/migrate/20140116231608_add_website_url_to_users.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddWebsiteUrlToUsers < ActiveRecord::Migration
- def change
- add_column :users, :website_url, :string, {:null => false, :default => ''}
- end
-end
diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb
deleted file mode 100644
index 6c7a92b6950..00000000000
--- a/db/migrate/20140122112253_create_merge_request_diffs.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# rubocop:disable all
-class CreateMergeRequestDiffs < ActiveRecord::Migration
- DOWNTIME = false
-
- def up
- create_table :merge_request_diffs do |t|
- t.string :state, null: false, default: 'collected'
- t.text :st_commits, null: true
- t.text :st_diffs, null: true
- t.integer :merge_request_id, null: false
-
- t.timestamps null: true
- end
-
- if ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
- change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647
- change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
- end
- end
-
- def down
- drop_table :merge_request_diffs
- end
-end
diff --git a/db/migrate/20140122114406_migrate_mr_diffs.rb b/db/migrate/20140122114406_migrate_mr_diffs.rb
deleted file mode 100644
index 429aeb2293f..00000000000
--- a/db/migrate/20140122114406_migrate_mr_diffs.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# rubocop:disable all
-class MigrateMrDiffs < ActiveRecord::Migration
- def self.up
- execute "INSERT INTO merge_request_diffs ( merge_request_id, st_commits, st_diffs ) SELECT id, st_commits, st_diffs FROM merge_requests"
- end
-
- def self.down
- MergeRequestDiff.delete_all
- end
-end
diff --git a/db/migrate/20140122122549_remove_m_rdiff_fields.rb b/db/migrate/20140122122549_remove_m_rdiff_fields.rb
deleted file mode 100644
index bbf35811b61..00000000000
--- a/db/migrate/20140122122549_remove_m_rdiff_fields.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# rubocop:disable all
-class RemoveMRdiffFields < ActiveRecord::Migration
- def up
- remove_column :merge_requests, :st_commits
- remove_column :merge_requests, :st_diffs
- end
-
- def down
- add_column :merge_requests, :st_commits, :text, null: true, limit: 2147483647
- add_column :merge_requests, :st_diffs, :text, null: true, limit: 2147483647
-
- if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
- execute "UPDATE merge_requests mr
- SET (st_commits, st_diffs) = (md.st_commits, md.st_diffs)
- FROM merge_request_diffs md
- WHERE md.merge_request_id = mr.id"
- else
- execute "UPDATE merge_requests mr, merge_request_diffs md SET mr.st_commits = md.st_commits WHERE md.merge_request_id = mr.id"
- execute "UPDATE merge_requests mr, merge_request_diffs md SET mr.st_diffs = md.st_diffs WHERE md.merge_request_id = mr.id"
- end
- end
-end
diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb
deleted file mode 100644
index 888341b7535..00000000000
--- a/db/migrate/20140125162722_add_avatar_to_projects.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddAvatarToProjects < ActiveRecord::Migration
- def change
- add_column :projects, :avatar, :string
- end
-end
diff --git a/db/migrate/20140127170938_add_group_avatars.rb b/db/migrate/20140127170938_add_group_avatars.rb
deleted file mode 100644
index 95d1c1c6b27..00000000000
--- a/db/migrate/20140127170938_add_group_avatars.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddGroupAvatars < ActiveRecord::Migration
- def change
- add_column :namespaces, :avatar, :string
- end
-end
diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb
deleted file mode 100644
index 51886f8fc89..00000000000
--- a/db/migrate/20140209025651_create_emails.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# rubocop:disable all
-class CreateEmails < ActiveRecord::Migration
- DOWNTIME = false
-
- def change
- create_table :emails do |t|
- t.integer :user_id, null: false
- t.string :email, null: false
-
- t.timestamps null: true
- end
-
- add_index :emails, :user_id
- add_index :emails, :email, unique: true
- end
-end
diff --git a/db/migrate/20140214102325_add_api_key_to_services.rb b/db/migrate/20140214102325_add_api_key_to_services.rb
deleted file mode 100644
index b58c36c0a30..00000000000
--- a/db/migrate/20140214102325_add_api_key_to_services.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddApiKeyToServices < ActiveRecord::Migration
- def change
- add_column :services, :api_key, :string
- end
-end
diff --git a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb
deleted file mode 100644
index aab8a41c2c3..00000000000
--- a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddIndexMergeRequestDiffsOnMergeRequestId < ActiveRecord::Migration
- def change
- add_index :merge_request_diffs, :merge_request_id, unique: true
- end
-end
diff --git a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb
deleted file mode 100644
index ec163bb843c..00000000000
--- a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddTagPushHooksToProjectHook < ActiveRecord::Migration
- def change
- add_column :web_hooks, :tag_push_events, :boolean, default: false
- end
-end
diff --git a/db/migrate/20140312145357_add_import_status_to_project.rb b/db/migrate/20140312145357_add_import_status_to_project.rb
deleted file mode 100644
index 9947cd8c6f9..00000000000
--- a/db/migrate/20140312145357_add_import_status_to_project.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# rubocop:disable all
-class AddImportStatusToProject < ActiveRecord::Migration
- def change
- add_column :projects, :import_status, :string
- end
-end
diff --git a/db/migrate/20140313092127_init_schema.rb b/db/migrate/20140313092127_init_schema.rb
new file mode 100644
index 00000000000..895298ed4ed
--- /dev/null
+++ b/db/migrate/20140313092127_init_schema.rb
@@ -0,0 +1,337 @@
+class InitSchema < ActiveRecord::Migration
+ DOWNTIME = true
+
+ def up
+ create_table "broadcast_messages", force: :cascade do |t|
+ t.text "message", null: false
+ t.datetime "starts_at"
+ t.datetime "ends_at"
+ t.integer "alert_type"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "color"
+ t.string "font"
+ end
+ create_table "deploy_keys_projects", force: :cascade do |t|
+ t.integer "deploy_key_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
+ create_table "emails", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "email", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ 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 "events", force: :cascade do |t|
+ t.string "target_type"
+ t.integer "target_id"
+ t.string "title"
+ t.text "data"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "action"
+ t.integer "author_id"
+ end
+ add_index "events", ["action"], name: "index_events_on_action", using: :btree
+ add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
+ add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
+ add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree
+ add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
+ add_index "events", ["target_type"], name: "index_events_on_target_type", 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"
+ 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 "issues", force: :cascade do |t|
+ t.string "title"
+ t.integer "assignee_id"
+ t.integer "author_id"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "position", default: 0
+ t.string "branch_name"
+ t.text "description"
+ t.integer "milestone_id"
+ t.string "state"
+ t.integer "iid"
+ end
+ add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
+ add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
+ add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
+ add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
+ add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
+ add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
+ create_table "keys", force: :cascade do |t|
+ t.integer "user_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.text "key"
+ t.string "title"
+ t.string "type"
+ t.string "fingerprint"
+ end
+ add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
+ create_table "merge_request_diffs", force: :cascade do |t|
+ t.string "state", default: "collected", null: false
+ t.text "st_commits"
+ t.text "st_diffs"
+ t.integer "merge_request_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
+ create_table "merge_requests", force: :cascade do |t|
+ t.string "target_branch", null: false
+ t.string "source_branch", null: false
+ t.integer "source_project_id", null: false
+ t.integer "author_id"
+ t.integer "assignee_id"
+ t.string "title"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "milestone_id"
+ t.string "state"
+ t.string "merge_status"
+ t.integer "target_project_id", null: false
+ t.integer "iid"
+ t.text "description"
+ 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", ["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"], name: "index_merge_requests_on_source_project_id", using: :btree
+ add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
+ add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
+ create_table "milestones", force: :cascade do |t|
+ t.string "title", null: false
+ t.integer "project_id", null: false
+ t.text "description"
+ t.date "due_date"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "state"
+ t.integer "iid"
+ end
+ add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
+ add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
+ create_table "namespaces", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "path", null: false
+ t.integer "owner_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "type"
+ t.string "description", default: "", null: false
+ t.string "avatar"
+ end
+ add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree
+ add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
+ add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
+ add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
+ create_table "notes", force: :cascade do |t|
+ t.text "note"
+ t.string "noteable_type"
+ t.integer "author_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "project_id"
+ t.string "attachment"
+ t.string "line_code"
+ t.string "commit_id"
+ t.integer "noteable_id"
+ t.boolean "system", default: false, null: false
+ t.text "st_diff"
+ 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", ["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", ["project_id"], name: "index_notes_on_project_id", using: :btree
+ create_table "project_group_links", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "group_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "group_access", default: 30, null: false
+ end
+ create_table "projects", force: :cascade do |t|
+ t.string "name"
+ t.string "path"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "creator_id"
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "wall_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
+ t.integer "namespace_id"
+ t.string "issues_tracker", default: "gitlab", null: false
+ t.string "issues_tracker_id"
+ t.boolean "snippets_enabled", default: true, null: false
+ t.datetime "last_activity_at"
+ t.string "import_url"
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
+ t.string "avatar"
+ t.string "import_status"
+ end
+ add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
+ add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
+ add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", 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"
+ end
+ add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
+ create_table "services", force: :cascade do |t|
+ t.string "type"
+ t.string "title"
+ t.string "token"
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
+ t.string "project_url"
+ t.string "subdomain"
+ t.string "room"
+ t.text "recipients"
+ t.string "api_key"
+ end
+ add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
+ create_table "snippets", force: :cascade do |t|
+ t.string "title"
+ t.text "content"
+ t.integer "author_id", null: false
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "file_name"
+ t.datetime "expires_at"
+ t.boolean "private", default: true, null: false
+ t.string "type"
+ end
+ add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
+ add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
+ add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
+ add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
+ create_table "taggings", force: :cascade do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context"
+ t.datetime "created_at"
+ end
+ 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
+ create_table "tags", force: :cascade do |t|
+ t.string "name"
+ end
+ create_table "users", force: :cascade do |t|
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
+ t.string "reset_password_token"
+ t.datetime "reset_password_sent_at"
+ t.datetime "remember_created_at"
+ t.integer "sign_in_count", default: 0
+ t.datetime "current_sign_in_at"
+ t.datetime "last_sign_in_at"
+ t.string "current_sign_in_ip"
+ t.string "last_sign_in_ip"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "admin", default: false, null: false
+ t.integer "projects_limit", default: 10
+ t.string "skype", default: "", null: false
+ t.string "linkedin", default: "", null: false
+ t.string "twitter", default: "", null: false
+ t.string "authentication_token"
+ t.integer "theme_id", default: 1, null: false
+ t.string "bio"
+ t.integer "failed_attempts", default: 0
+ t.datetime "locked_at"
+ t.string "extern_uid"
+ t.string "provider"
+ t.string "username"
+ t.boolean "can_create_group", default: true, null: false
+ t.boolean "can_create_team", default: true, null: false
+ t.string "state"
+ t.integer "color_scheme_id", default: 1, null: false
+ t.integer "notification_level", default: 1, null: false
+ t.datetime "password_expires_at"
+ t.integer "created_by_id"
+ t.datetime "last_credential_check_at"
+ t.string "avatar"
+ t.string "confirmation_token"
+ t.datetime "confirmed_at"
+ t.datetime "confirmation_sent_at"
+ t.string "unconfirmed_email"
+ t.boolean "hide_no_ssh_key", default: false
+ t.string "website_url", default: "", null: false
+ end
+ add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
+ add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
+ add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
+ add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
+ add_index "users", ["extern_uid", "provider"], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree
+ add_index "users", ["name"], name: "index_users_on_name", using: :btree
+ add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
+ add_index "users", ["username"], name: "index_users_on_username", using: :btree
+ create_table "users_groups", force: :cascade do |t|
+ t.integer "group_access", null: false
+ t.integer "group_id", null: false
+ t.integer "user_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "notification_level", default: 3, null: false
+ end
+ add_index "users_groups", ["user_id"], name: "index_users_groups_on_user_id", using: :btree
+ create_table "users_projects", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "project_access", default: 0, null: false
+ t.integer "notification_level", default: 3, null: false
+ end
+ add_index "users_projects", ["project_access"], name: "index_users_projects_on_project_access", using: :btree
+ add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree
+ add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree
+ create_table "web_hooks", force: :cascade do |t|
+ t.string "url"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "type", default: "ProjectHook"
+ t.integer "service_id"
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ end
+ add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable"
+ end
+end
diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb
deleted file mode 100644
index 0afc26b8764..00000000000
--- a/db/migrate/20140313092127_migrate_already_imported_projects.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# rubocop:disable all
-class MigrateAlreadyImportedProjects < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
-
- def up
- execute("UPDATE projects SET import_status = 'finished' WHERE imported = #{true_value}")
- execute("UPDATE projects SET import_status = 'none' WHERE imported = #{false_value}")
- remove_column :projects, :imported
- end
-
- def down
- add_column :projects, :imported, :boolean, default: false
- execute("UPDATE projects SET imported = #{true_value} WHERE import_status = 'finished'")
- end
-end
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
index bddc234db25..17357b67ab7 100644
--- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -59,17 +59,17 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration
end
def move_namespace(group_id, path_was, path)
- repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path
+ repository_storages = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
+ row['repository_storage']
end.compact
# Move the namespace directory in all storages paths used by member projects
- repository_storage_paths.each do |repository_storage_path|
+ repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, path_was)
+ gitlab_shell.add_namespace(repository_storage, path_was)
- unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+ unless gitlab_shell.mv_namespace(repository_storage, path_was, path)
+ Rails.logger.error "Exception moving on shard #{repository_storage} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index 7c28d934c29..8986cd8cb4b 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -53,8 +53,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end
- def path_exists?(path, repository_storage_path)
- repository_storage_path && gitlab_shell.exists?(repository_storage_path, path)
+ def path_exists?(shard, repository_storage_path)
+ repository_storage_path && gitlab_shell.exists?(shard, repository_storage_path)
end
# Accepts invalid path like test.git and returns test_git or
@@ -70,8 +70,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
def check_routes(base, counter, path)
route_exists = route_exists?(path)
- Gitlab.config.repositories.storages.each_value do |storage|
- if route_exists || path_exists?(path, storage.legacy_disk_path)
+ Gitlab.config.repositories.storages.each do |shard, storage|
+ if route_exists || path_exists?(shard, storage.legacy_disk_path)
counter += 1
path = "#{base}#{counter}"
@@ -83,17 +83,17 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
end
def move_namespace(namespace_id, path_was, path)
- repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path
+ repository_storages = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
+ row['repository_storage']
end.compact
- # Move the namespace directory in all storages paths used by member projects
- repository_storage_paths.each do |repository_storage_path|
+ # Move the namespace directory in all storages used by member projects
+ repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, path_was)
+ gitlab_shell.add_namespace(repository_storage, path_was)
- unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+ unless gitlab_shell.mv_namespace(repository_storage, path_was, path)
+ Rails.logger.error "Exception moving on shard #{repository_storage} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
diff --git a/db/migrate/20170301101006_add_ci_runner_namespaces.rb b/db/migrate/20170301101006_add_ci_runner_namespaces.rb
new file mode 100644
index 00000000000..deaf03e928b
--- /dev/null
+++ b/db/migrate/20170301101006_add_ci_runner_namespaces.rb
@@ -0,0 +1,17 @@
+class AddCiRunnerNamespaces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :ci_runner_namespaces do |t|
+ t.integer :runner_id
+ t.integer :namespace_id
+
+ t.index [:runner_id, :namespace_id], unique: true
+ t.index :namespace_id
+ t.foreign_key :ci_runners, column: :runner_id, on_delete: :cascade
+ t.foreign_key :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20170906133745_add_runners_token_to_groups.rb b/db/migrate/20170906133745_add_runners_token_to_groups.rb
new file mode 100644
index 00000000000..852f4cba670
--- /dev/null
+++ b/db/migrate/20170906133745_add_runners_token_to_groups.rb
@@ -0,0 +1,9 @@
+class AddRunnersTokenToGroups < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :runners_token, :string
+ end
+end
diff --git a/db/migrate/20180326202229_create_ci_build_trace_chunks.rb b/db/migrate/20180326202229_create_ci_build_trace_chunks.rb
new file mode 100644
index 00000000000..fb3f5786e85
--- /dev/null
+++ b/db/migrate/20180326202229_create_ci_build_trace_chunks.rb
@@ -0,0 +1,17 @@
+class CreateCiBuildTraceChunks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :ci_build_trace_chunks, id: :bigserial 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.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
+ t.index [:build_id, :chunk_index], unique: true
+ end
+ end
+end
diff --git a/db/migrate/20180403035759_create_project_ci_cd_settings.rb b/db/migrate/20180403035759_create_project_ci_cd_settings.rb
new file mode 100644
index 00000000000..06856af6204
--- /dev/null
+++ b/db/migrate/20180403035759_create_project_ci_cd_settings.rb
@@ -0,0 +1,68 @@
+class CreateProjectCiCdSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:project_ci_cd_settings)
+ create_table(:project_ci_cd_settings) do |t|
+ t.integer(:project_id, null: false)
+ t.boolean(:group_runners_enabled, default: true, null: false)
+ end
+ end
+
+ disable_statement_timeout
+
+ # This particular INSERT will take between 10 and 20 seconds.
+ execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects'
+
+ # We add the index and foreign key separately so the above INSERT statement
+ # takes as little time as possible.
+ add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true)
+
+ add_foreign_key_with_retry
+ end
+
+ def down
+ drop_table :project_ci_cd_settings
+ end
+
+ def add_foreign_key_with_retry
+ if Gitlab::Database.mysql?
+ # When using MySQL we don't support online upgrades, thus projects can't
+ # be deleted while we are running this migration.
+ return add_project_id_foreign_key
+ end
+
+ # Between the initial INSERT and the addition of the foreign key some
+ # projects may have been removed, leaving orphaned rows in our new settings
+ # table.
+ loop do
+ remove_orphaned_settings
+
+ begin
+ add_project_id_foreign_key
+ break
+ rescue ActiveRecord::InvalidForeignKey
+ say 'project_ci_cd_settings contains some orphaned rows, retrying...'
+ end
+ end
+ end
+
+ def add_project_id_foreign_key
+ add_concurrent_foreign_key(:project_ci_cd_settings, :projects, column: :project_id)
+ end
+
+ def remove_orphaned_settings
+ execute <<~SQL
+ DELETE FROM project_ci_cd_settings
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM projects
+ WHERE projects.id = project_ci_cd_settings.project_id
+ )
+ SQL
+ end
+end
diff --git a/db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb b/db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb
new file mode 100644
index 00000000000..0f2734853e6
--- /dev/null
+++ b/db/migrate/20180406204716_add_limits_ci_build_trace_chunks_raw_data_for_mysql.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+require Rails.root.join('db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql')
+
+class AddLimitsCiBuildTraceChunksRawDataForMysql < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ LimitsCiBuildTraceChunksRawDataForMysql.new.up
+ 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
new file mode 100644
index 00000000000..8fc558be733
--- /dev/null
+++ b/db/migrate/20180413022611_create_missing_namespace_for_internal_users.rb
@@ -0,0 +1,66 @@
+class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ connection.exec_query(users_query.to_sql).rows.each do |id, username|
+ create_namespace(id, username)
+ # When testing locally I've noticed that these internal users are missing
+ # the notification email, for more details visit the below link:
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18357#note_68327560
+ set_notification_email(id)
+ end
+ end
+
+ def down
+ # no-op
+ end
+
+ private
+
+ def users
+ @users ||= Arel::Table.new(:users)
+ end
+
+ def namespaces
+ @namespaces ||= Arel::Table.new(:namespaces)
+ end
+
+ def users_query
+ condition = users[:ghost].eq(true)
+
+ if column_exists?(:users, :support_bot)
+ condition = condition.or(users[:support_bot].eq(true))
+ end
+
+ users.join(namespaces, Arel::Nodes::OuterJoin)
+ .on(namespaces[:type].eq(nil).and(namespaces[:owner_id].eq(users[:id])))
+ .where(namespaces[:owner_id].eq(nil))
+ .where(condition)
+ .project(users[:id], users[:username])
+ end
+
+ def create_namespace(user_id, username)
+ path = Uniquify.new.string(username) do |str|
+ query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1"
+ connection.exec_query(query).present?
+ end
+
+ insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')"
+ namespace_id = connection.insert_sql(insert_query)
+
+ create_route(namespace_id)
+ end
+
+ def create_route(namespace_id)
+ return unless namespace_id
+
+ 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}')")
+ end
+
+ def set_notification_email(user_id)
+ execute "UPDATE users SET notification_email = email WHERE notification_email IS NULL AND id = #{user_id}"
+ end
+end
diff --git a/db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb b/db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb
new file mode 100644
index 00000000000..37e2d19e022
--- /dev/null
+++ b/db/migrate/20180416155103_add_further_scope_columns_to_internal_id_table.rb
@@ -0,0 +1,15 @@
+class AddFurtherScopeColumnsToInternalIdTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_null :internal_ids, :project_id, true
+ add_column :internal_ids, :namespace_id, :integer, null: true
+ end
+
+ def down
+ change_column_null :internal_ids, :project_id, false
+ remove_column :internal_ids, :namespace_id
+ end
+end
diff --git a/db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb b/db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb
new file mode 100644
index 00000000000..582b89a3948
--- /dev/null
+++ b/db/migrate/20180417090132_add_index_constraints_to_internal_id_table.rb
@@ -0,0 +1,40 @@
+class AddIndexConstraintsToInternalIdTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :internal_ids, [:usage, :namespace_id], unique: true, where: 'namespace_id IS NOT NULL'
+
+ replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
+ add_concurrent_index :internal_ids, [:usage, :project_id], unique: true, where: 'project_id IS NOT NULL'
+ end
+
+ add_concurrent_foreign_key :internal_ids, :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+
+ def down
+ remove_concurrent_index :internal_ids, [:usage, :namespace_id]
+
+ replace_index(:internal_ids, [:usage, :project_id], name: 'index_internal_ids_on_usage_and_project_id') do
+ add_concurrent_index :internal_ids, [:usage, :project_id], unique: true
+ end
+
+ remove_foreign_key :internal_ids, column: :namespace_id
+ end
+
+ private
+ def replace_index(table, columns, name:)
+ temporary_name = "#{name}_old"
+
+ if index_exists?(table, columns, name: name)
+ rename_index table, name, temporary_name
+ end
+
+ yield
+
+ remove_concurrent_index_by_name table, temporary_name
+ end
+end
diff --git a/db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb b/db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb
new file mode 100644
index 00000000000..ee82c70ecf8
--- /dev/null
+++ b/db/migrate/20180417101040_add_tmp_stage_priority_index_to_ci_builds.rb
@@ -0,0 +1,16 @@
+class AddTmpStagePriorityIndexToCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:ci_builds, [:stage_id, :stage_idx],
+ where: 'stage_idx IS NOT NULL', name: 'tmp_build_stage_position_index')
+ end
+
+ def down
+ remove_concurrent_index_by_name(:ci_builds, 'tmp_build_stage_position_index')
+ end
+end
diff --git a/db/migrate/20180417101940_add_index_to_ci_stage.rb b/db/migrate/20180417101940_add_index_to_ci_stage.rb
new file mode 100644
index 00000000000..9dac78db774
--- /dev/null
+++ b/db/migrate/20180417101940_add_index_to_ci_stage.rb
@@ -0,0 +1,9 @@
+class AddIndexToCiStage < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_stages, :position, :integer
+ end
+end
diff --git a/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb b/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb
new file mode 100644
index 00000000000..1084ca14a34
--- /dev/null
+++ b/db/migrate/20180418053107_add_index_to_ci_job_artifacts_file_store.rb
@@ -0,0 +1,15 @@
+class AddIndexToCiJobArtifactsFileStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_job_artifacts, :file_store
+ end
+
+ def down
+ remove_index :ci_job_artifacts, :file_store if index_exists?(:ci_job_artifacts, :file_store)
+ end
+end
diff --git a/db/migrate/20180420010016_add_pipeline_build_foreign_key.rb b/db/migrate/20180420010016_add_pipeline_build_foreign_key.rb
new file mode 100644
index 00000000000..6fabe07bc9c
--- /dev/null
+++ b/db/migrate/20180420010016_add_pipeline_build_foreign_key.rb
@@ -0,0 +1,27 @@
+class AddPipelineBuildForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ execute <<~SQL
+ DELETE FROM ci_builds WHERE project_id IS NULL OR commit_id IS NULL
+ SQL
+
+ execute <<~SQL
+ DELETE FROM ci_builds WHERE NOT EXISTS
+ (SELECT true FROM ci_pipelines WHERE ci_pipelines.id = ci_builds.commit_id)
+ AND stage_id IS NULL
+ SQL
+
+ add_concurrent_foreign_key(:ci_builds, :ci_pipelines, column: :commit_id)
+ end
+
+ def down
+ return unless foreign_key_exists?(:ci_builds, :ci_pipelines, column: :commit_id)
+
+ remove_foreign_key(:ci_builds, column: :commit_id)
+ end
+end
diff --git a/db/migrate/20180420010616_cleanup_build_stage_migration.rb b/db/migrate/20180420010616_cleanup_build_stage_migration.rb
new file mode 100644
index 00000000000..24777294101
--- /dev/null
+++ b/db/migrate/20180420010616_cleanup_build_stage_migration.rb
@@ -0,0 +1,61 @@
+class CleanupBuildStageMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ TMP_INDEX = 'tmp_id_stage_partial_null_index'.freeze
+
+ disable_ddl_transaction!
+
+ class Build < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'ci_builds'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def up
+ disable_statement_timeout
+
+ ##
+ # We steal from the background migrations queue to catch up with the
+ # scheduled migrations set.
+ #
+ Gitlab::BackgroundMigration.steal('MigrateBuildStage')
+
+ ##
+ # We add temporary index, to make iteration over batches more performant.
+ # Conditional here is to avoid the need of doing that in a separate
+ # migration file to make this operation idempotent.
+ #
+ unless index_exists_by_name?(:ci_builds, TMP_INDEX)
+ add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', name: TMP_INDEX)
+ end
+
+ ##
+ # We check if there are remaining rows that should be migrated (for example
+ # if Sidekiq / Redis fails / is restarted, what could result in not all
+ # background migrations being executed correctly.
+ #
+ # We migrate remaining rows synchronously in a blocking way, to make sure
+ # that when this migration is done we are confident that all rows are
+ # already migrated.
+ #
+ Build.where('stage_id IS NULL').each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::MigrateBuildStage.new.perform(*range)
+ end
+
+ ##
+ # We remove temporary index, because it is not required during standard
+ # operations and runtime.
+ #
+ remove_concurrent_index_by_name(:ci_builds, TMP_INDEX)
+ end
+
+ def down
+ if index_exists_by_name?(:ci_builds, TMP_INDEX)
+ remove_concurrent_index_by_name(:ci_builds, TMP_INDEX)
+ end
+ end
+end
diff --git a/db/migrate/20180424090541_add_enforce_terms_to_application_settings.rb b/db/migrate/20180424090541_add_enforce_terms_to_application_settings.rb
new file mode 100644
index 00000000000..306cd737771
--- /dev/null
+++ b/db/migrate/20180424090541_add_enforce_terms_to_application_settings.rb
@@ -0,0 +1,9 @@
+class AddEnforceTermsToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :enforce_terms, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20180424134533_create_application_setting_terms.rb b/db/migrate/20180424134533_create_application_setting_terms.rb
new file mode 100644
index 00000000000..f29335cfc51
--- /dev/null
+++ b/db/migrate/20180424134533_create_application_setting_terms.rb
@@ -0,0 +1,13 @@
+class CreateApplicationSettingTerms < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :application_setting_terms do |t|
+ t.integer :cached_markdown_version
+ t.text :terms, null: false
+ t.text :terms_html
+ end
+ end
+end
diff --git a/db/migrate/20180425075446_create_term_agreements.rb b/db/migrate/20180425075446_create_term_agreements.rb
new file mode 100644
index 00000000000..22a9d7b574d
--- /dev/null
+++ b/db/migrate/20180425075446_create_term_agreements.rb
@@ -0,0 +1,28 @@
+class CreateTermAgreements < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :term_agreements do |t|
+ t.references :term, index: true, null: false
+ t.foreign_key :application_setting_terms, column: :term_id
+ t.references :user, index: true, null: false, foreign_key: { on_delete: :cascade }
+ t.boolean :accepted, default: false, null: false
+
+ t.timestamps_with_timezone null: false
+ end
+
+ add_index :term_agreements, [:user_id, :term_id],
+ unique: true,
+ name: 'term_agreements_unique_index'
+ end
+
+ def down
+ remove_index :term_agreements, name: 'term_agreements_unique_index'
+
+ drop_table :term_agreements
+ end
+end
diff --git a/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb b/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb
new file mode 100644
index 00000000000..0e991c23bfa
--- /dev/null
+++ b/db/migrate/20180425131009_assure_commits_count_for_merge_request_diff.rb
@@ -0,0 +1,27 @@
+class AssureCommitsCountForMergeRequestDiff < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class MergeRequestDiff < ActiveRecord::Base
+ self.table_name = 'merge_request_diffs'
+
+ include ::EachBatch
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('AddMergeRequestDiffCommitsCount')
+
+ MergeRequestDiff.where(commits_count: nil).each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount.new.perform(*range)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20180426102016_add_accepted_term_to_users.rb b/db/migrate/20180426102016_add_accepted_term_to_users.rb
new file mode 100644
index 00000000000..3d446f66214
--- /dev/null
+++ b/db/migrate/20180426102016_add_accepted_term_to_users.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddAcceptedTermToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ change_table :users do |t|
+ t.references :accepted_term,
+ null: true
+ end
+ add_concurrent_foreign_key :users, :application_setting_terms, column: :accepted_term_id
+ end
+
+ def down
+ remove_foreign_key :users, column: :accepted_term_id
+ remove_column :users, :accepted_term_id
+ end
+end
diff --git a/db/migrate/20180430101916_add_runner_type_to_ci_runners.rb b/db/migrate/20180430101916_add_runner_type_to_ci_runners.rb
new file mode 100644
index 00000000000..42409349b75
--- /dev/null
+++ b/db/migrate/20180430101916_add_runner_type_to_ci_runners.rb
@@ -0,0 +1,9 @@
+class AddRunnerTypeToCiRunners < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_runners, :runner_type, :smallint
+ end
+end
diff --git a/db/migrate/20180502122856_create_project_mirror_data.rb b/db/migrate/20180502122856_create_project_mirror_data.rb
new file mode 100644
index 00000000000..d449f944844
--- /dev/null
+++ b/db/migrate/20180502122856_create_project_mirror_data.rb
@@ -0,0 +1,20 @@
+class CreateProjectMirrorData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ return if table_exists?(:project_mirror_data)
+
+ create_table :project_mirror_data do |t|
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }
+ t.string :status
+ t.string :jid
+ t.text :last_error
+ end
+ end
+
+ def down
+ drop_table(:project_mirror_data) if table_exists?(:project_mirror_data)
+ end
+end
diff --git a/db/migrate/20180503131624_create_remote_mirrors.rb b/db/migrate/20180503131624_create_remote_mirrors.rb
new file mode 100644
index 00000000000..7800186455f
--- /dev/null
+++ b/db/migrate/20180503131624_create_remote_mirrors.rb
@@ -0,0 +1,33 @@
+class CreateRemoteMirrors < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ return if table_exists?(:remote_mirrors)
+
+ create_table :remote_mirrors do |t|
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }
+ t.string :url
+ t.boolean :enabled, default: true
+ t.string :update_status
+ t.datetime :last_update_at
+ t.datetime :last_successful_update_at
+ t.datetime :last_update_started_at
+ t.string :last_error
+ t.boolean :only_protected_branches, default: false, null: false
+ t.string :remote_name
+ t.text :encrypted_credentials
+ t.string :encrypted_credentials_iv
+ t.string :encrypted_credentials_salt
+
+ t.timestamps null: false
+ end
+ end
+
+ def down
+ drop_table(:remote_mirrors) if table_exists?(:remote_mirrors)
+ end
+end
diff --git a/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb b/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb
new file mode 100644
index 00000000000..841393971f4
--- /dev/null
+++ b/db/migrate/20180503141722_add_remote_mirror_available_overridden_to_projects.rb
@@ -0,0 +1,15 @@
+class AddRemoteMirrorAvailableOverriddenToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column(:projects, :remote_mirror_available_overridden, :boolean) unless column_exists?(:projects, :remote_mirror_available_overridden)
+ end
+
+ def down
+ remove_column(:projects, :remote_mirror_available_overridden) if column_exists?(:projects, :remote_mirror_available_overridden)
+ end
+end
diff --git a/db/migrate/20180503150427_add_index_to_namespaces_runners_token.rb b/db/migrate/20180503150427_add_index_to_namespaces_runners_token.rb
new file mode 100644
index 00000000000..4c4e576d49f
--- /dev/null
+++ b/db/migrate/20180503150427_add_index_to_namespaces_runners_token.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToNamespacesRunnersToken < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :namespaces, :runners_token, unique: true
+ end
+
+ def down
+ if index_exists?(:namespaces, :runners_token, unique: true)
+ remove_index :namespaces, :runners_token
+ end
+ end
+end
diff --git a/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb
new file mode 100644
index 00000000000..970a53d68d0
--- /dev/null
+++ b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb
@@ -0,0 +1,15 @@
+class EnsureMissingColumnsToProjectMirrorData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status)
+ add_column :project_mirror_data, :jid, :string unless column_exists?(:project_mirror_data, :jid)
+ add_column :project_mirror_data, :last_error, :text unless column_exists?(:project_mirror_data, :last_error)
+ end
+
+ def down
+ # db/migrate/20180502122856_create_project_mirror_data.rb will remove the table
+ end
+end
diff --git a/db/migrate/20180503175054_add_indexes_to_project_mirror_data.rb b/db/migrate/20180503175054_add_indexes_to_project_mirror_data.rb
new file mode 100644
index 00000000000..17570269b2e
--- /dev/null
+++ b/db/migrate/20180503175054_add_indexes_to_project_mirror_data.rb
@@ -0,0 +1,17 @@
+class AddIndexesToProjectMirrorData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :project_mirror_data, :jid
+ add_concurrent_index :project_mirror_data, :status
+ end
+
+ def down
+ remove_index :project_mirror_data, :jid if index_exists? :project_mirror_data, :jid
+ remove_index :project_mirror_data, :status if index_exists? :project_mirror_data, :status
+ end
+end
diff --git a/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb b/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb
new file mode 100644
index 00000000000..9a9decffdab
--- /dev/null
+++ b/db/migrate/20180503193542_add_indexes_to_remote_mirror.rb
@@ -0,0 +1,15 @@
+class AddIndexesToRemoteMirror < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :remote_mirrors, :last_successful_update_at unless index_exists?(:remote_mirrors, :last_successful_update_at)
+ end
+
+ def down
+ remove_index :remote_mirrors, :last_successful_update_at if index_exists? :remote_mirrors, :last_successful_update_at
+ end
+end
diff --git a/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
new file mode 100644
index 00000000000..25b9905b1a9
--- /dev/null
+++ b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
@@ -0,0 +1,15 @@
+class AddMirrorAvailableToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :mirror_available, :boolean, default: true, allow_null: false) unless column_exists?(:application_settings, :mirror_available)
+ end
+
+ def down
+ remove_column(:application_settings, :mirror_available) if column_exists?(:application_settings, :mirror_available)
+ end
+end
diff --git a/db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb b/db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb
new file mode 100644
index 00000000000..2c8f86ff0f4
--- /dev/null
+++ b/db/migrate/20180503200320_enable_prometheus_metrics_by_default.rb
@@ -0,0 +1,11 @@
+class EnablePrometheusMetricsByDefault < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ change_column_default :application_settings, :prometheus_metrics_enabled, true
+ end
+
+ def down
+ change_column_default :application_settings, :prometheus_metrics_enabled, false
+ end
+end
diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb
new file mode 100644
index 00000000000..d6f25d3d4ab
--- /dev/null
+++ b/db/migrate/20180504195842_project_name_lower_index.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ProjectNameLowerIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+ INDEX_NAME = 'index_projects_on_lower_name'
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+
+ execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))"
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+
+ if supports_drop_index_concurrently?
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
+ else
+ execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
+ end
+ end
+end
diff --git a/db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb b/db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb
new file mode 100644
index 00000000000..0d8a8357a1e
--- /dev/null
+++ b/db/migrate/20180508055821_make_remote_mirrors_disabled_by_default.rb
@@ -0,0 +1,11 @@
+class MakeRemoteMirrorsDisabledByDefault < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ change_column_default :remote_mirrors, :enabled, false
+ end
+
+ def down
+ change_column_default :remote_mirrors, :enabled, true
+ end
+end
diff --git a/db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb b/db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb
new file mode 100644
index 00000000000..82087d15ccb
--- /dev/null
+++ b/db/migrate/20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb
@@ -0,0 +1,21 @@
+class AddNotNullConstraintToProjectMirrorDataForeignKey < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class ProjectImportState < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_mirror_data'
+ end
+
+ def up
+ ProjectImportState.where(project_id: nil).delete_all
+
+ change_column_null :project_mirror_data, :project_id, false
+ end
+
+ def down
+ change_column_null :project_mirror_data, :project_id, true
+ end
+end
diff --git a/db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb b/db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb
new file mode 100644
index 00000000000..acb976b52fa
--- /dev/null
+++ b/db/migrate/20180508102840_add_unique_constraint_to_project_mirror_data_project_id_index.rb
@@ -0,0 +1,31 @@
+class AddUniqueConstraintToProjectMirrorDataProjectIdIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:project_mirror_data,
+ :project_id,
+ unique: true,
+ name: 'index_project_mirror_data_on_project_id_unique')
+
+ remove_concurrent_index_by_name(:project_mirror_data, 'index_project_mirror_data_on_project_id')
+
+ rename_index(:project_mirror_data,
+ 'index_project_mirror_data_on_project_id_unique',
+ 'index_project_mirror_data_on_project_id')
+ end
+
+ def down
+ rename_index(:project_mirror_data,
+ 'index_project_mirror_data_on_project_id',
+ 'index_project_mirror_data_on_project_id_old')
+
+ add_concurrent_index(:project_mirror_data, :project_id)
+
+ remove_concurrent_index_by_name(:project_mirror_data,
+ 'index_project_mirror_data_on_project_id_old')
+ end
+end
diff --git a/db/migrate/20180508135515_set_runner_type_not_null.rb b/db/migrate/20180508135515_set_runner_type_not_null.rb
new file mode 100644
index 00000000000..dd043ec7179
--- /dev/null
+++ b/db/migrate/20180508135515_set_runner_type_not_null.rb
@@ -0,0 +1,9 @@
+class SetRunnerTypeNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ change_column_null(:ci_runners, :runner_type, false)
+ end
+end
diff --git a/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb b/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb
new file mode 100644
index 00000000000..580f56007c7
--- /dev/null
+++ b/db/migrate/20180511090724_add_index_on_ci_runners_runner_type.rb
@@ -0,0 +1,15 @@
+class AddIndexOnCiRunnersRunnerType < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_runners, :runner_type
+ end
+
+ def down
+ remove_index :ci_runners, :runner_type
+ end
+end
diff --git a/db/migrate/20180515121227_create_notes_diff_files.rb b/db/migrate/20180515121227_create_notes_diff_files.rb
new file mode 100644
index 00000000000..7108bc1a64b
--- /dev/null
+++ b/db/migrate/20180515121227_create_notes_diff_files.rb
@@ -0,0 +1,21 @@
+class CreateNotesDiffFiles < ActiveRecord::Migration
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ create_table :note_diff_files do |t|
+ t.references :diff_note, references: :notes, null: false, index: { unique: true }
+ t.text :diff, null: false
+ t.boolean :new_file, null: false
+ t.boolean :renamed_file, null: false
+ t.boolean :deleted_file, null: false
+ t.string :a_mode, null: false
+ t.string :b_mode, null: false
+ t.text :new_path, null: false
+ t.text :old_path, null: false
+ end
+
+ add_foreign_key :note_diff_files, :notes, column: :diff_note_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb
new file mode 100644
index 00000000000..3b7b877232b
--- /dev/null
+++ b/db/migrate/20180517082340_add_not_null_constraints_to_project_authorizations.rb
@@ -0,0 +1,38 @@
+class AddNotNullConstraintsToProjectAuthorizations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ if Gitlab::Database.postgresql?
+ # One-pass version for PostgreSQL
+ execute <<~SQL
+ ALTER TABLE project_authorizations
+ ALTER COLUMN user_id SET NOT NULL,
+ ALTER COLUMN project_id SET NOT NULL,
+ ALTER COLUMN access_level SET NOT NULL
+ SQL
+ else
+ change_column_null :project_authorizations, :user_id, false
+ change_column_null :project_authorizations, :project_id, false
+ change_column_null :project_authorizations, :access_level, false
+ end
+ end
+
+ def down
+ if Gitlab::Database.postgresql?
+ # One-pass version for PostgreSQL
+ execute <<~SQL
+ ALTER TABLE project_authorizations
+ ALTER COLUMN user_id DROP NOT NULL,
+ ALTER COLUMN project_id DROP NOT NULL,
+ ALTER COLUMN access_level DROP NOT NULL
+ SQL
+ else
+ change_column_null :project_authorizations, :user_id, true
+ change_column_null :project_authorizations, :project_id, true
+ change_column_null :project_authorizations, :access_level, true
+ end
+ end
+end
diff --git a/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb
new file mode 100644
index 00000000000..df84898003f
--- /dev/null
+++ b/db/migrate/20180521171529_increase_mysql_text_limit_for_gpg_keys.rb
@@ -0,0 +1,2 @@
+# rubocop:disable all
+require_relative 'gpg_keys_limits_to_mysql'
diff --git a/db/migrate/gpg_keys_limits_to_mysql.rb b/db/migrate/gpg_keys_limits_to_mysql.rb
new file mode 100644
index 00000000000..780340d0564
--- /dev/null
+++ b/db/migrate/gpg_keys_limits_to_mysql.rb
@@ -0,0 +1,15 @@
+class IncreaseMysqlTextLimitForGpgKeys < ActiveRecord::Migration
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ return unless Gitlab::Database.mysql?
+
+ change_column :gpg_keys, :key, :text, limit: 16.megabytes - 1
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb b/db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb
new file mode 100644
index 00000000000..e1771912c3c
--- /dev/null
+++ b/db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql.rb
@@ -0,0 +1,9 @@
+class LimitsCiBuildTraceChunksRawDataForMysql < ActiveRecord::Migration
+ def up
+ return unless Gitlab::Database.mysql?
+
+ # Mysql needs MEDIUMTEXT type (up to 16MB) rather than TEXT (up to 64KB)
+ # Because 'raw_data' is always capped by Ci::BuildTraceChunk::CHUNK_SIZE, which is 128KB
+ change_column :ci_build_trace_chunks, :raw_data, :binary, limit: 16.megabytes - 1 #MEDIUMTEXT
+ end
+end
diff --git a/db/optional_migrations/composite_primary_keys.rb b/db/optional_migrations/composite_primary_keys.rb
new file mode 100644
index 00000000000..0fd3fca52dd
--- /dev/null
+++ b/db/optional_migrations/composite_primary_keys.rb
@@ -0,0 +1,63 @@
+# This migration adds a primary key constraint to tables
+# that only have a composite unique key.
+#
+# This is not strictly relevant to Rails (v4 does not
+# support composite primary keys). However this becomes
+# useful for e.g. PostgreSQL's logical replication (pglogical)
+# which requires all tables to have a primary key constraint.
+#
+# In that sense, the migration is optional and not strictly needed.
+class CompositePrimaryKeysMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ Index = Struct.new(:table, :name, :columns)
+
+ TABLES = [
+ Index.new(:issue_assignees, 'index_issue_assignees_on_issue_id_and_user_id', %i(issue_id user_id)),
+ Index.new(:user_interacted_projects, 'index_user_interacted_projects_on_project_id_and_user_id', %i(project_id user_id)),
+ Index.new(:merge_request_diff_files, 'index_merge_request_diff_files_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)),
+ Index.new(:merge_request_diff_commits, 'index_merge_request_diff_commits_on_mr_diff_id_and_order', %i(merge_request_diff_id relative_order)),
+ Index.new(:project_authorizations, 'index_project_authorizations_on_user_id_project_id_access_level', %i(user_id project_id access_level)),
+ Index.new(:push_event_payloads, 'index_push_event_payloads_on_event_id', %i(event_id)),
+ Index.new(:schema_migrations, 'unique_schema_migrations', %(version)),
+ ]
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+ TABLES.each do |index|
+ add_primary_key(index)
+ end
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+ TABLES.each do |index|
+ remove_primary_key(index)
+ end
+ end
+
+ private
+ def add_primary_key(index)
+ execute "ALTER TABLE #{index.table} ADD PRIMARY KEY USING INDEX #{index.name}"
+ end
+
+ def remove_primary_key(index)
+ temp_index_name = "#{index.name[0..58]}_old"
+ rename_index index.table, index.name, temp_index_name if index_exists_by_name?(index.table, index.name)
+
+ # re-create unique key index
+ add_concurrent_index index.table, index.columns, unique: true, name: index.name
+
+ # This also drops the `temp_index_name` as this is owned by the constraint
+ execute "ALTER TABLE #{index.table} DROP CONSTRAINT IF EXISTS #{temp_index_name}"
+ end
+end
+
diff --git a/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb
new file mode 100644
index 00000000000..3b0fdb3aeea
--- /dev/null
+++ b/db/post_migrate/20180409170809_populate_missing_project_ci_cd_settings.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PopulateMissingProjectCiCdSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # MySQL does not support online upgrades, thus there can't be any missing
+ # rows.
+ return if Gitlab::Database.mysql?
+
+ # Projects created after the initial migration but before the code started
+ # using ProjectCiCdSetting won't have a corresponding row in
+ # project_ci_cd_settings, so let's fix that.
+ execute <<~SQL
+ INSERT INTO project_ci_cd_settings (project_id)
+ SELECT id
+ FROM projects
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM project_ci_cd_settings
+ WHERE project_ci_cd_settings.project_id = projects.id
+ )
+ SQL
+ end
+
+ def down
+ # There's nothing to revert for this migration.
+ end
+end
diff --git a/db/post_migrate/20180420080616_schedule_stages_index_migration.rb b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb
new file mode 100644
index 00000000000..1d0daad002f
--- /dev/null
+++ b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb
@@ -0,0 +1,29 @@
+class ScheduleStagesIndexMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'MigrateStageIndex'.freeze
+ BATCH_SIZE = 10000
+
+ disable_ddl_transaction!
+
+ class Stage < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_stages'
+ end
+
+ def up
+ disable_statement_timeout
+
+ Stage.all.tap do |relation|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ MIGRATION,
+ 5.minutes,
+ batch_size: BATCH_SIZE)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20180430143705_backfill_runner_type_for_ci_runners_post_migrate.rb b/db/post_migrate/20180430143705_backfill_runner_type_for_ci_runners_post_migrate.rb
new file mode 100644
index 00000000000..38af5aae924
--- /dev/null
+++ b/db/post_migrate/20180430143705_backfill_runner_type_for_ci_runners_post_migrate.rb
@@ -0,0 +1,23 @@
+class BackfillRunnerTypeForCiRunnersPostMigrate < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ INSTANCE_RUNNER_TYPE = 1
+ PROJECT_RUNNER_TYPE = 3
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:ci_runners, :runner_type, INSTANCE_RUNNER_TYPE) do |table, query|
+ query.where(table[:is_shared].eq(true)).where(table[:runner_type].eq(nil))
+ end
+
+ update_column_in_batches(:ci_runners, :runner_type, PROJECT_RUNNER_TYPE) do |table, query|
+ query.where(table[:is_shared].eq(false)).where(table[:runner_type].eq(nil))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20180502134117_migrate_import_attributes_data_from_projects_to_project_mirror_data.rb b/db/post_migrate/20180502134117_migrate_import_attributes_data_from_projects_to_project_mirror_data.rb
new file mode 100644
index 00000000000..e39cd33c414
--- /dev/null
+++ b/db/post_migrate/20180502134117_migrate_import_attributes_data_from_projects_to_project_mirror_data.rb
@@ -0,0 +1,38 @@
+class MigrateImportAttributesDataFromProjectsToProjectMirrorData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ UP_MIGRATION = 'PopulateImportState'.freeze
+ DOWN_MIGRATION = 'RollbackImportStateData'.freeze
+
+ BATCH_SIZE = 1000
+ DELAY_INTERVAL = 5.minutes
+
+ disable_ddl_transaction!
+
+ class Project < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'projects'
+ end
+
+ class ProjectImportState < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_mirror_data'
+ end
+
+ def up
+ projects = Project.where.not(import_status: :none)
+
+ queue_background_migration_jobs_by_range_at_intervals(projects, UP_MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ import_state = ProjectImportState.where.not(status: :none)
+
+ queue_background_migration_jobs_by_range_at_intervals(import_state, DOWN_MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+end
diff --git a/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb b/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb
new file mode 100644
index 00000000000..88a9f5f8256
--- /dev/null
+++ b/db/post_migrate/20180511174224_add_unique_constraint_to_project_features_project_id.rb
@@ -0,0 +1,43 @@
+class AddUniqueConstraintToProjectFeaturesProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class ProjectFeature < ActiveRecord::Base
+ self.table_name = 'project_features'
+
+ include EachBatch
+ end
+
+ def up
+ remove_duplicates
+
+ add_concurrent_index :project_features, :project_id, unique: true, name: 'index_project_features_on_project_id_unique'
+ remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id'
+ rename_index :project_features, 'index_project_features_on_project_id_unique', 'index_project_features_on_project_id'
+ end
+
+ def down
+ rename_index :project_features, 'index_project_features_on_project_id', 'index_project_features_on_project_id_old'
+ add_concurrent_index :project_features, :project_id
+ remove_concurrent_index_by_name :project_features, 'index_project_features_on_project_id_old'
+ end
+
+ private
+
+ def remove_duplicates
+ features = ProjectFeature
+ .select('MAX(id) AS max, COUNT(id), project_id')
+ .group(:project_id)
+ .having('COUNT(id) > 1')
+
+ features.each do |feature|
+ ProjectFeature
+ .where(project_id: feature['project_id'])
+ .where('id <> ?', feature['max'])
+ .each_batch { |batch| batch.delete_all }
+ end
+ end
+end
diff --git a/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb b/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb
new file mode 100644
index 00000000000..5a6d6ff4a10
--- /dev/null
+++ b/db/post_migrate/20180512061621_add_not_null_constraint_to_project_features_project_id.rb
@@ -0,0 +1,21 @@
+class AddNotNullConstraintToProjectFeaturesProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class ProjectFeature < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_features'
+ end
+
+ def up
+ ProjectFeature.where(project_id: nil).delete_all
+
+ change_column_null :project_features, :project_id, false
+ end
+
+ def down
+ change_column_null :project_features, :project_id, true
+ end
+end
diff --git a/db/post_migrate/20180514161336_remove_gemnasium_service.rb b/db/post_migrate/20180514161336_remove_gemnasium_service.rb
new file mode 100644
index 00000000000..6d7806e8daa
--- /dev/null
+++ b/db/post_migrate/20180514161336_remove_gemnasium_service.rb
@@ -0,0 +1,15 @@
+class RemoveGemnasiumService < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ disable_statement_timeout
+
+ execute("DELETE FROM services WHERE type='GemnasiumService';")
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 87f30f59d43..884e333874c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180405142733) do
+ActiveRecord::Schema.define(version: 20180521171529) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -40,6 +40,12 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.text "new_project_guidelines_html"
end
+ create_table "application_setting_terms", force: :cascade do |t|
+ t.integer "cached_markdown_version"
+ t.text "terms", null: false
+ t.text "terms_html"
+ end
+
create_table "application_settings", force: :cascade do |t|
t.integer "default_projects_limit"
t.boolean "signup_enabled"
@@ -128,7 +134,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.integer "cached_markdown_version"
t.boolean "clientside_sentry_enabled", default: false, null: false
t.string "clientside_sentry_dsn"
- t.boolean "prometheus_metrics_enabled", default: false, null: false
+ t.boolean "prometheus_metrics_enabled", default: true, null: false
t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url"
t.integer "performance_bar_allowed_group_id"
@@ -158,6 +164,8 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.string "auto_devops_domain"
t.boolean "pages_domain_verification_enabled", default: true, null: false
t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false
+ t.boolean "enforce_terms", default: false
+ t.boolean "mirror_available", default: true, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -246,6 +254,15 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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"
+ 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
@@ -322,6 +339,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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", ["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", ["status"], name: "index_ci_builds_on_status", using: :btree
@@ -367,6 +385,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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
@@ -442,6 +461,14 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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"
+ 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"
@@ -470,11 +497,13 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.integer "access_level", default: 0, null: false
t.string "ip_address"
t.integer "maximum_timeout"
+ t.integer "runner_type", limit: 2, null: false
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|
@@ -485,6 +514,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.string "name"
t.integer "status"
t.integer "lock_version"
+ t.integer "position"
end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree
@@ -895,12 +925,14 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "internal_ids", id: :bigserial, force: :cascade do |t|
- t.integer "project_id", null: false
+ t.integer "project_id"
t.integer "usage", null: false
t.integer "last_value", null: false
+ t.integer "namespace_id"
end
- add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, using: :btree
+ 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
@@ -1256,6 +1288,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.boolean "require_two_factor_authentication", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false
t.integer "cached_markdown_version"
+ t.string "runners_token"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -1266,8 +1299,23 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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
+ t.boolean "new_file", null: false
+ t.boolean "renamed_file", null: false
+ t.boolean "deleted_file", null: false
+ t.string "a_mode", null: false
+ t.string "b_mode", null: false
+ t.text "new_path", null: false
+ t.text "old_path", null: false
+ 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"
@@ -1415,9 +1463,9 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "project_authorizations", id: false, force: :cascade do |t|
- t.integer "user_id"
- t.integer "project_id"
- t.integer "access_level"
+ t.integer "user_id", null: false
+ t.integer "project_id", null: false
+ t.integer "access_level", null: false
end
add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
@@ -1433,6 +1481,13 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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
+ 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 "created_at", null: false
t.datetime "updated_at", null: false
@@ -1453,7 +1508,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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"
+ t.integer "project_id", null: false
t.integer "merge_requests_access_level"
t.integer "issues_access_level"
t.integer "wiki_access_level"
@@ -1464,7 +1519,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.integer "repository_access_level", default: 20, null: false
end
- add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree
+ 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
@@ -1488,6 +1543,17 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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"
+ 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
@@ -1552,6 +1618,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.boolean "merge_requests_rebase_enabled", default: false, null: false
t.integer "jobs_cache_index"
t.boolean "pages_https_only", default: true
+ t.boolean "remote_mirror_available_overridden"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1657,6 +1724,27 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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"
+ t.boolean "enabled", default: false
+ t.string "update_status"
+ t.datetime "last_update_at"
+ t.datetime "last_successful_update_at"
+ t.datetime "last_update_started_at"
+ t.string "last_error"
+ t.boolean "only_protected_branches", default: false, null: false
+ t.string "remote_name"
+ t.text "encrypted_credentials"
+ t.string "encrypted_credentials_iv"
+ t.string "encrypted_credentials_salt"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ 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 "routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
@@ -1794,6 +1882,18 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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
+ 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"
@@ -1982,6 +2082,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.string "preferred_language"
t.string "rss_token"
t.integer "theme_id", limit: 2
+ t.integer "accepted_term_id"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -2056,11 +2157,13 @@ ActiveRecord::Schema.define(version: 20180405142733) do
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
+ add_foreign_key "ci_build_trace_chunks", "ci_builds", column: "build_id", on_delete: :cascade
add_foreign_key "ci_build_trace_section_names", "projects", on_delete: :cascade
add_foreign_key "ci_build_trace_sections", "ci_build_trace_section_names", column: "section_name_id", name: "fk_264e112c66", on_delete: :cascade
add_foreign_key "ci_build_trace_sections", "ci_builds", column: "build_id", name: "fk_4ebe41f502", on_delete: :cascade
add_foreign_key "ci_build_trace_sections", "projects", on_delete: :cascade
add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
+ add_foreign_key "ci_builds", "ci_pipelines", column: "commit_id", name: "fk_d3130c9a7f", on_delete: :cascade
add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
@@ -2075,6 +2178,8 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify
add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify
add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade
+ add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade
+ add_foreign_key "ci_runner_namespaces", "namespaces", on_delete: :cascade
add_foreign_key "ci_runner_projects", "projects", name: "fk_4478a6f1e4", on_delete: :cascade
add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade
add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade
@@ -2112,6 +2217,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade
add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
@@ -2151,6 +2257,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
+ add_foreign_key "note_diff_files", "notes", column: "diff_note_id", on_delete: :cascade
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
@@ -2158,12 +2265,14 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
+ add_foreign_key "project_ci_cd_settings", "projects", name: "fk_24c15d2f2e", on_delete: :cascade
add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
+ add_foreign_key "project_mirror_data", "projects", on_delete: :cascade
add_foreign_key "project_statistics", "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
@@ -2174,10 +2283,13 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade
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 "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
+ add_foreign_key "term_agreements", "application_setting_terms", column: "term_id"
+ add_foreign_key "term_agreements", "users", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "todos", "notes", name: "fk_91d1f47b13", on_delete: :cascade
@@ -2191,6 +2303,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade
add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
+ add_foreign_key "users", "application_setting_terms", column: "accepted_term_id", name: "fk_789cd90b35", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index a841a4cfbf1..ff8dd3fab8a 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
---
# GitLab Documentation
@@ -15,8 +16,8 @@ To understand what features you have access to, check the [GitLab subscriptions]
| General documentation | GitLab CI/CD docs |
| :----- | :----- |
-| [User documentation](user/index.md) | [GitLab CI/CD](ci/README.md) |
-| [Administrator documentation](administration/index.md) | [GitLab CI/CD quick start guide](ci/quick_start/README.md) |
+| [User documentation](user/index.md) | [GitLab CI/CD quick start guide](ci/quick_start/README.md) |
+| [Administrator documentation](administration/index.md) | [GitLab CI/CD examples](ci/examples/README.md) |
| [Contributor documentation](#contributor-documentation) | [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) |
| [Getting started with GitLab](#getting-started-with-gitlab) | [Using Docker images](ci/docker/using_docker_images.md) |
| [API](api/README.md) | [Auto DevOps](topics/autodevops/index.md) |
@@ -90,6 +91,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Create a file](user/project/repository/web_editor.md#create-a-file)
- [Upload a file](user/project/repository/web_editor.md#upload-a-file)
- [File templates](user/project/repository/web_editor.md#template-dropdowns)
+ - [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files)
- [Create a directory](user/project/repository/web_editor.md#create-a-directory)
- [Start a merge request](user/project/repository/web_editor.md#tips) (when committing via UI)
- [Branches](user/project/repository/branches/index.md)
@@ -100,6 +102,14 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Commits](user/project/repository/index.md#commits)
- [Signing commits](user/project/repository/gpg_signed_commits/index.md): use GPG to sign your commits.
+#### Merge Requests
+
+- [Merge Requests](user/project/merge_requests/index.md)
+ - [Work In Progress "WIP" Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md)
+ - [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved.
+ - [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally)
+ - [Cherry-pick](user/project/merge_requests/cherry_pick_changes.md)
+
#### Integrations
- [Project Services](user/project/integrations/project_services.md): Integrate a project with external services, such as CI and chat.
@@ -113,18 +123,16 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
### Verify
-Spot errors sooner and shorten feedback cycles with built-in code review, code testing,
-Code Quality, and Review Apps. Customize your approval workflow controls, automatically
-test the quality of your code, and spin up a staging environment for every code change.
-GitLab Continuous Integration is the most popular next generation testing system that
-auto scales to run your tests faster.
+Spot errors sooner, improve security and shorten feedback cycles with built-in
+static code analysis, code testing, code quality, dependency checking and review
+apps. Customize your approval workflow controls, automatically test the quality
+of your code, and spin up a staging environment for every code change. GitLab
+Continuous Integration is the most popular next generation testing system that
+scales to run your tests faster.
-- [Merge Requests](user/project/merge_requests/index.md)
- - [Work In Progress Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md)
- - [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved.
- - [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally)
- - [Cherry-pick](user/project/merge_requests/cherry_pick_changes.md)
+- [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
- [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request.
+- [Pipeline Graphs](ci/pipelines.md#pipeline-graphs)
### Package
@@ -132,7 +140,6 @@ GitLab Container Registry gives you the enhanced security and access controls of
custom Docker images without 3rd party add-ons. Easily upload and download images
from GitLab CI/CD with full Git repository management integration.
-- [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
- [GitLab Container Registry](user/project/container_registry.md): Learn how to use GitLab's built-in Container Registry.
### Release
@@ -141,9 +148,11 @@ Spend less time configuring your tools, and more time creating. Whether you’re
deploying to one server or thousands, build, test, and release your code
confidently and securely with GitLab’s built-in Continuous Delivery and Deployment.
-- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy a static site directly from GitLab.
- [Auto Deploy](topics/autodevops/index.md#auto-deploy): Configure GitLab CI for the deployment of your application.
- [Environments and deployments](ci/environments.md): With environments, you can control the continuous deployment of your software within GitLab.
+- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy a static site directly from GitLab.
+- [Scheduled Pipelines](user/project/pipelines/schedules.md)
+- [Protected Runners](ci/runners/README.md#protected-runners)
### Configure
@@ -152,6 +161,9 @@ Auto Devops. Best practice templates get you started with minimal to zero
configuration. Then customize everything from buildpacks to CI/CD.
- [Auto DevOps](topics/autodevops/index.md)
+- [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications)
+- [Protected secret variables](ci/variables/README.md#protected-secret-variables)
+- [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab)
### Monitor
@@ -229,7 +241,7 @@ GitLab.com is hosted, managed, and administered by GitLab, Inc., with
and teams: Free, Bronze, Silver, and Gold.
GitLab.com subscriptions grants access
-to the same features available in GitLab self-hosted, **expect
+to the same features available in GitLab self-hosted, **except
[administration](administration/index.md) tools and settings**:
- GitLab.com Free includes the same features available in Core
diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md
index b51e705ab52..8b00f52ffc1 100644
--- a/doc/administration/auth/jwt.md
+++ b/doc/administration/auth/jwt.md
@@ -50,7 +50,7 @@ JWT will provide you with a secret key for you to use.
required_claims: ["name", "email"],
info_map: { name: "name", email: "email" },
auth_url: 'https://example.com/',
- valid_within: nil,
+ valid_within: null,
}
}
```
diff --git a/doc/administration/external_database.md b/doc/administration/external_database.md
new file mode 100644
index 00000000000..31199f2cdc7
--- /dev/null
+++ b/doc/administration/external_database.md
@@ -0,0 +1,17 @@
+# Configure GitLab using an external PostgreSQL service
+
+If you're hosting GitLab on a cloud provider, you can optionally use a
+managed service for PostgreSQL. For example, AWS offers a managed Relational
+Database Service (RDS) that runs PostgreSQL.
+
+Alternatively, you may opt to manage your own PostgreSQL instance or cluster
+separate from the GitLab Omnibus package.
+
+If you use a cloud-managed service, or provide your own PostgreSQL instance:
+
+1. Setup PostgreSQL according to the
+ [database requirements document](../install/requirements.md#database).
+1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
+ needs privileges to create the `gitlabhq_production` database.
+1. Configure the GitLab application servers with the appropriate details.
+ This step is covered in [Configuring GitLab for HA](high_availability/gitlab.md).
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index d8928a7fe4c..957f17e3ea3 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -1,7 +1,7 @@
# NFS
You can view information and options set for each of the mounted NFS file
-systems by running `sudo nfsstat -m`.
+systems by running `nfsstat -m` and `cat /etc/fstab`.
## NFS Server features
@@ -75,43 +75,33 @@ Notice several options that you should consider using:
| `nobootwait` | Don't halt boot process waiting for this mount to become available
| `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously.
-## Mount locations
+## A single NFS mount
-When using default Omnibus configuration you will need to share 5 data locations
-between all GitLab cluster nodes. No other locations should be shared. The
-following are the 5 locations you need to mount:
-
-| Location | Description | Default configuration |
-| -------- | ----------- | --------------------- |
-| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
-| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
-| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
-| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
-| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
+It's recommended to nest all gitlab data dirs within a mount, that allows automatic
+restore of backups without manually moving existing data.
-Other GitLab directories should not be shared between nodes. They contain
-node-specific files and GitLab code that does not need to be shared. To ship
-logs to a central location consider using remote syslog. GitLab Omnibus packages
-provide configuration for [UDP log shipping][udp-log-shipping].
-
-### Consolidating mount points
-
-If you don't want to configure 5-6 different NFS mount points, you have a few
-alternative options.
+```
+mountpoint
+└── gitlab-data
+ ├── builds
+ ├── git-data
+ ├── home-git
+ ├── shared
+ └── uploads
+```
-#### Change default file locations
+To do so, we'll need to configure Omnibus with the paths to each directory nested
+in the mount point as follows:
-Omnibus allows you to configure the file locations. With custom configuration
-you can specify just one main mountpoint and have all of these locations
-as subdirectories. Mount `/gitlab-data` then use the following Omnibus
+Mount `/gitlab-nfs` then use the following Omnibus
configuration to move each data location to a subdirectory:
```ruby
-git_data_dirs({"default" => "/gitlab-data/git-data"})
-user['home'] = '/gitlab-data/home'
-gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
-gitlab_rails['shared_path'] = '/gitlab-data/shared'
-gitlab_ci['builds_directory'] = '/gitlab-data/builds'
+git_data_dirs({"default" => "/gitlab-nfs/gitlab-data/git-data"})
+user['home'] = '/gitlab-nfs/gitlab-data/home'
+gitlab_rails['uploads_directory'] = '/gitlab-nfs/gitlab-data/uploads'
+gitlab_rails['shared_path'] = '/gitlab-nfs/gitlab-data/shared'
+gitlab_ci['builds_directory'] = '/gitlab-nfs/gitlab-data/builds'
```
To move the `git` home directory, all GitLab services must be stopped. Run
@@ -122,22 +112,52 @@ Run `sudo gitlab-ctl reconfigure` to start using the central location. Please
be aware that if you had existing data you will need to manually copy/rsync it
to these new locations and then restart GitLab.
-#### Bind mounts
+## Bind mounts
+
+Alternatively to changing the configuration in Omnibus, bind mounts can be used
+to store the data on an NFS mount.
Bind mounts provide a way to specify just one NFS mount and then
bind the default GitLab data locations to the NFS mount. Start by defining your
single NFS mount point as you normally would in `/etc/fstab`. Let's assume your
-NFS mount point is `/gitlab-data`. Then, add the following bind mounts in
+NFS mount point is `/gitlab-nfs`. Then, add the following bind mounts in
`/etc/fstab`:
```bash
-/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0
-/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0
-/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0
-/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0
-/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0
+/gitlab-nfs/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0
+/gitlab-nfs/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0
+/gitlab-nfs/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0
+/gitlab-nfs/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0
+/gitlab-nfs/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0
```
+Using bind mounts will require manually making sure the data directories
+are empty before attempting a restore. Read more about the
+[restore prerequisites](../../raketasks/backup_restore.md).
+
+## Multiple NFS mounts
+
+When using default Omnibus configuration you will need to share 5 data locations
+between all GitLab cluster nodes. No other locations should be shared. The
+following are the 5 locations need to be shared:
+
+| Location | Description | Default configuration |
+| -------- | ----------- | --------------------- |
+| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
+| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
+| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
+| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
+| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
+
+Other GitLab directories should not be shared between nodes. They contain
+node-specific files and GitLab code that does not need to be shared. To ship
+logs to a central location consider using remote syslog. GitLab Omnibus packages
+provide configuration for [UDP log shipping][udp-log-shipping].
+
+Having multiple NFS mounts will require manually making sure the data directories
+are empty before attempting a restore. Read more about the
+[restore prerequisites](../../raketasks/backup_restore.md).
+
---
Read more on high-availability configuration:
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index fd2677996b1..031fb31ca4f 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -323,7 +323,7 @@ The prerequisites for a HA Redis setup are the following:
# machines to connect to it.
redis['port'] = 6379
- # The same password for Redeis authentication you set up for the master node.
+ # The same password for Redis authentication you set up for the master node.
redis['password'] = 'redis-password-goes-here'
# The IP of the master Redis node.
@@ -650,6 +650,47 @@ gitlab_rails['redis_sentinels'] = [
Omnibus GitLab configures some things behind the curtains to make the sysadmins'
lives easier. If you want to know what happens underneath keep reading.
+### Running multiple Redis clusters
+
+GitLab supports running [separate Redis clusters for different persistent
+classes](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances):
+cache, queues, and shared_state. To make this work with Sentinel:
+
+1. Set the appropriate variable in `/etc/gitlab/gitlab.rb` for each instance you are using:
+
+ ```ruby
+ gitlab_rails['redis_cache_instance'] = REDIS_CACHE_URL
+ gitlab_rails['redis_queues_instance'] = REDIS_QUEUES_URL
+ gitlab_rails['redis_shared_state_instance'] = REDIS_SHARED_STATE_URL
+ ```
+ **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. Include an array of hashes with host/port combinations, such as the following:
+
+ ```ruby
+ gitlab_rails['redis_cache_sentinels'] = [
+ { host: REDIS_CACHE_SENTINEL_HOST, port: PORT1 },
+ { host: REDIS_CACHE_SENTINEL_HOST2, port: PORT2 }
+ ]
+ gitlab_rails['redis_queues_sentinels'] = [
+ { host: REDIS_QUEUES_SENTINEL_HOST, port: PORT1 },
+ { host: REDIS_QUEUES_SENTINEL_HOST2, port: PORT2 }
+ ]
+ gitlab_rails['redis_shared_state_sentinels'] = [
+ { host: SHARED_STATE_SENTINEL_HOST, port: PORT1 },
+ { host: SHARED_STATE_SENTINEL_HOST2, port: PORT2 }
+ ]
+ ```
+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.
+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.
+1. Run `gitlab-ctl reconfigure`
+
### Control running services
In the previous example, we've used `redis_sentinel_role` and
diff --git a/doc/administration/index.md b/doc/administration/index.md
index b472ca5b4d8..df935095e61 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Learn how to install, configure, update, and maintain your GitLab instance.'
+---
+
# Administrator documentation **[CORE ONLY]**
Learn how to administer your GitLab instance (Community Edition and
@@ -40,6 +44,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
[source installations](../install/installation.md#installation-from-source).
- [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
- [Plugins](plugins.md): With custom plugins, GitLab administrators can introduce custom integrations without modifying GitLab's source code.
+- [Enforcing Terms of Service](../user/admin_area/settings/terms.md)
#### Customizing GitLab's appearance
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 896cb93e5ed..77fe4d561a1 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -107,7 +107,7 @@ For source installations the following settings are nested under `artifacts:` an
| Setting | Description | Default |
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
-| `remote_directory` | The bucket name where Artfacts will be stored| |
+| `remote_directory` | The bucket name where Artifacts will be stored| |
| `direct_upload` | Set to true to enable direct upload of Artifacts without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. Currently only `Google` provider is supported | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
@@ -148,7 +148,7 @@ _The artifacts are stored by default in
```
NOTE: For GitLab 9.4+, if you are using AWS IAM profiles, be sure to omit the
- AWS access key and secret acces key/value pairs. For example:
+ AWS access key and secret access key/value pairs. For example:
```ruby
gitlab_rails['artifacts_object_store_connection'] = {
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index 84a1ffeec98..f0b2054a7f3 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -40,3 +40,98 @@ To change the location where the job logs will be stored, follow the steps below
[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"
+
+## New live trace architecture
+
+> [Introduced][ce-18169] in GitLab 10.4.
+
+> **Notes**:
+- This feature is still Beta, which could impact GitLab.com/on-premises instances, and in the worst case scenario, traces will be lost.
+- This feature is still being discussed in [an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/46097) for the performance improvements.
+- This feature is off by default. Please check below how to enable/disable this featrue.
+
+**What is "live trace"?**
+
+Job trace that is sent by runner while jobs are running. You can see live trace in job pages UI.
+The live traces are archived once job finishes.
+
+**What is new architecture?**
+
+So far, when GitLab Runner sends a job trace to GitLab-Rails, traces have been saved to file storage as text files.
+This was a problem for [Cloud Native-compatible GitLab application](https://gitlab.com/gitlab-com/migration/issues/23) where GitLab had to rely on File Storage.
+
+This new live trace architecture stores chunks of traces in Redis and database instead of file storage.
+Redis is used as first-class storage, and it stores up-to 128kB. Once the full chunk is sent it will be flushed to database. Afterwhile, the data in Redis and database will be archived to ObjectStorage.
+
+Here is the detailed data flow.
+
+1. GitLab Runner picks a job from GitLab-Rails
+1. GitLab Runner sends a piece of trace to GitLab-Rails
+1. GitLab-Rails appends the data to Redis
+1. If the data in Redis is fulfilled 128kB, the data is flushed to Database.
+1. 2.~4. is continued until the job is finished
+1. Once the job is finished, GitLab-Rails schedules a sidekiq worker to archive the trace
+1. The sidekiq worker archives the trace to Object Storage, and cleanup the trace in Redis and Database
+
+**How to check if it's on or off?**
+
+```ruby
+Feature.enabled?('ci_enable_live_trace')
+```
+
+**How to enable?**
+
+```ruby
+Feature.enable('ci_enable_live_trace')
+```
+
+>**Note:**
+The transition period will be handled gracefully. Upcoming traces will be generated with the new architecture, and on-going live traces will stay with the legacy architecture (i.e. on-going live traces won't be re-generated forcibly with the new architecture).
+
+**How to disable?**
+
+```ruby
+Feature.disable('ci_enable_live_trace')
+```
+
+>**Note:**
+The transition period will be handled gracefully. Upcoming traces will be generated with the legacy architecture, and on-going live traces will stay with the new architecture (i.e. on-going live traces won't be re-generated forcibly with the legacy architecture).
+
+**Redis namespace:**
+
+`Gitlab::Redis::SharedState`
+
+**Potential impact:**
+
+- This feature could incur data loss:
+ - Case 1: When all data in Redis are accidentally flushed.
+ - On-going live traces could be recovered by re-sending traces (This is supported by all versions of GitLab Runner)
+ - Finished jobs which has not archived live traces will lose the last part (~128kB) of trace data.
+ - Case 2: When sidekiq workers failed to archive (e.g. There was a bug that prevents archiving process, Sidekiq inconsistancy, etc):
+ - Currently all trace data in Redis will be deleted after one week. If the sidekiq workers can't finish by the expiry date, the part of trace data will be lost.
+- This feature could consume all memory on Redis instance. If the number of jobs is 1000, 128MB (128kB * 1000) is consumed.
+- This feature could pressure Database replication lag. `INSERT` are generated to indicate that we have trace chunk. `UPDATE` with 128kB of data is issued once we receive multiple chunks.
+- and so on
+
+**How to test?**
+
+We're currently evaluating this feature on dev.gitalb.org or staging.gitlab.com to verify this features. Here is the list of tests/measurements.
+
+- Features:
+ - Live traces should be visible on job pages
+ - Archived traces should be visible on job pages
+ - Live traces should be archived to Object storage
+ - Live traces should be cleaned up after archived
+ - etc
+- Performance:
+ - Schedule 1000~10000 jobs and let GitLab-runners process concurrently. Measure memoery presssure, IO load, etc.
+ - etc
+- Failover:
+ - Simulate Redis outage
+ - etc
+
+**How to verify the correctnesss?**
+
+- TBD
+
+[ce-44935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index f495990d9a4..411a0fae93f 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -1,7 +1,7 @@
# GitLab Prometheus metrics
>**Note:**
-Available since [Omnibus GitLab 9.3][29118]. Currently experimental. For
+Available since [Omnibus GitLab 9.3][29118]. For
installations from source you'll have to configure it yourself.
To enable the GitLab Prometheus metrics:
@@ -24,7 +24,7 @@ server, because the embedded server configuration is overwritten once every
## Metrics available
-In this experimental phase, only a few metrics are available:
+The following metrics are available:
| Metric | Type | Since | Description |
|:--------------------------------- |:--------- |:----- |:----------- |
@@ -46,7 +46,7 @@ In this experimental phase, only a few metrics are available:
| 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 |
| filesystem_circuitbreaker_latency_seconds | Gauge | 9.5 | Time spent validating if a storage is accessible |
-| filesystem_circuitbreaker | Gauge | 9.5 | Wether or not the circuit for a certain shard is broken or not |
+| filesystem_circuitbreaker | Gauge | 9.5 | Whether or not the circuit for a certain shard is broken or not |
| circuitbreaker_storage_check_duration_seconds | Histogram | 10.3 | Time a single storage probe took |
## Metrics shared directory
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 3d24812c66a..f47add48345 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -120,7 +120,7 @@ To disable the monitoring of Kubernetes:
## GitLab Prometheus metrics
-> Introduced as an experimental feature in GitLab 9.3.
+> Introduced in GitLab 9.3.
GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic.
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index bd6c7bb07b5..89331238ce4 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -31,7 +31,7 @@ GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup
to the GitLab database. GitLab Shell uses the fingerprint of the SSH key to
check whether the user is authorized to access GitLab.
-Add the following to your `sshd_config` file. This is usuaully located at
+Add the following to your `sshd_config` file. This is usually located at
`/etc/ssh/sshd_config`, but it will be `/assets/sshd_config` if you're using
Omnibus Docker:
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 00c631fdaae..c0221533f13 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Learn how to administer GitLab Pages.'
+---
+
# GitLab Pages administration
> **Notes:**
@@ -8,8 +12,6 @@
GitLab from source, follow the [Pages source installation document](source.md).
- To learn how to use GitLab Pages, read the [user documentation][pages-userguide].
----
-
This document describes how to set up the _latest_ GitLab Pages feature. Make
sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
version as it may include new features and changes needed to be made in your
@@ -24,8 +26,6 @@ SNI and exposes pages using HTTP2 by default.
You are encouraged to read its [README][pages-readme] to fully understand how
it works.
----
-
In the case of [custom domains](#custom-domains) (but not
[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
ports `80` and/or `443`. For that reason, there is some flexibility in the way
@@ -119,11 +119,17 @@ The Pages daemon doesn't listen to the outside world.
1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
pages_external_url 'http://example.io'
```
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
+
Watch the [video tutorial][video-admin] for this configuration.
@@ -143,7 +149,7 @@ outside world.
1. Place the certificate and key inside `/etc/gitlab/ssl`
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
- ```ruby
+ ```shell
pages_external_url 'https://example.io'
pages_nginx['redirect_http_to_https'] = true
@@ -155,6 +161,11 @@ outside world.
respectively.
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
## Advanced configuration
@@ -180,7 +191,7 @@ world. Custom domains are supported, but no TLS.
1. Edit `/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
@@ -192,6 +203,11 @@ world. Custom domains are supported, but no TLS.
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
### Custom domains with TLS support
@@ -210,7 +226,7 @@ world. Custom domains and TLS are supported.
1. Edit `/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
pages_external_url "https://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
@@ -225,6 +241,11 @@ world. Custom domains and TLS are supported.
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
### Custom domain verification
@@ -247,11 +268,16 @@ are stored.
If you wish to store them in another location you must set it up in
`/etc/gitlab/gitlab.rb`:
- ```ruby
+ ```shell
gitlab_rails['pages_path'] = "/mnt/storage/pages"
```
1. [Reconfigure GitLab][reconfigure]
+1. Restart gitlab-pages by running the following command:
+
+ ```shell
+ sudo gitlab-ctl restart gitlab-pages
+ ```
## Set maximum pages size
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index 39b1883375e..ecc4ac6b29b 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -1,4 +1,4 @@
-# Project import/export
+# Project import/export administration **[CORE ONLY]**
>**Note:**
>
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index ee37ea49874..efeec9db517 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -13,12 +13,12 @@ checks failed you can see their output on the admin log page under
## Periodic checks
-When enabled, GitLab periodically runs a repository check on all project
-repositories and wiki repositories in order to detect data corruption problems.
+When enabled, GitLab periodically runs a repository check on all project
+repositories and wiki repositories in order to detect data corruption.
A project will be checked no more than once per month. If any projects
fail their repository checks all GitLab administrators will receive an email
-notification of the situation. This notification is sent out once a week on
-Sunday, by default.
+notification of the situation. This notification is sent out once a week,
+by default, midnight at the start of Sunday.
## Disabling periodic checks
@@ -28,16 +28,18 @@ panel.
## What to do if a check failed
If the repository check fails for some repository you should look up the error
-in repocheck.log (in the admin panel or on disk; see
-`/var/log/gitlab/gitlab-rails` for Omnibus installations or
-`/home/git/gitlab/log` for installations from source). Once you have
-resolved the issue use the admin panel to trigger a new repository check on
-the project. This will clear the 'check failed' state.
+in `repocheck.log`:
+
+- in the [admin panel](logs.md#repocheck.log)
+- or on disk, see:
+ - `/var/log/gitlab/gitlab-rails` for Omnibus installations
+ - `/home/git/gitlab/log` for installations from source
If for some reason the periodic repository check caused a lot of false
-alarms you can choose to clear ALL repository check states from the
-'Settings' page of the admin panel.
+alarms you can choose to clear *all* repository check states by
+clicking "Clear all repository checks" on the **Settings** page of the
+admin panel (`/admin/application_settings`).
---
[ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck"
-[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation"
+[git-fsck]: https://git-scm.com/docs/git-fsck "git fsck documentation"
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index 2fa3284b6be..7f0bd8f04e3 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -104,7 +104,7 @@ _The uploads are stored by default in
```
>**Note:**
-If you are using AWS IAM profiles, be sure to omit the AWS access key and secret acces key/value pairs.
+If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs.
```ruby
gitlab_rails['uploads_object_store_connection'] = {
diff --git a/doc/api/README.md b/doc/api/README.md
index ae4481b400e..194907accc7 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -13,6 +13,7 @@ following locations:
- [Broadcast Messages](broadcast_messages.md)
- [Project-level Variables](project_level_variables.md)
- [Group-level Variables](group_level_variables.md)
+- [Code Snippets](snippets.md)
- [Commits](commits.md)
- [Custom Attributes](custom_attributes.md)
- [Deployments](deployments.md)
@@ -32,6 +33,7 @@ following locations:
- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
+- [Markdown](markdown.md)
- [Merge Requests](merge_requests.md)
- [Project milestones](milestones.md)
- [Group milestones](group_milestones.md)
@@ -85,6 +87,29 @@ have been resolved to our satisfaction by the relicensing of the reference
implementations under MIT, and the use of the OWF license for the GraphQL
specification.
+## Compatibility Guidelines
+
+The HTTP API is versioned using a single number, the current one being 4. This
+number symbolises the same as the major version number as described by
+[SemVer](https://semver.org/). This mean that backward incompatible changes
+will require this version number to change. However, the minor version is
+not explicit. This allows for a stable API endpoint, but also means new
+features can be added to the API in the same version number.
+
+New features and bug fixes are released in tandem with a new GitLab, and apart
+from incidental patch and security releases, are released on the 22nd each
+month. Backward incompatible changes (e.g. endpoints removal, parameters
+removal etc.), as well as removal of entire API versions are done in tandem
+with a major point release of GitLab itself. All deprecations and changes
+between two versions should be listed in the documentation. For the changes
+between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md)
+
+#### Current status
+
+Currently two API versions are available, v3 and v4. v3 is deprecated and
+will soon be removed. Deletion is scheduled for
+[GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
+
## Basic usage
API requests should be prefixed with `api` and the API version. The API version
@@ -269,7 +294,7 @@ The following table gives an overview of how the API functions generally behave.
| `GET` | Access one or more resources and return the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
| `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
-| `DELETE` | Returns `204 No Content` if the resuource was deleted successfully. |
+| `DELETE` | Returns `204 No Content` if the resource was deleted successfully. |
The following table shows the possible return codes for API requests.
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index c341b7f2009..65e2f9d6cd9 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -1,6 +1,6 @@
# Discussions API
-Discussions are set of related notes on snippets or issues.
+Discussions are set of related notes on snippets, issues, merge requests or commits.
## Issues
@@ -61,7 +61,8 @@ GET /projects/:id/issues/:issue_iid/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Issue",
- "noteable_iid": null
+ "noteable_iid": null,
+ "resolvable": false
}
]
},
@@ -87,7 +88,8 @@ GET /projects/:id/issues/:issue_iid/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Issue",
- "noteable_iid": null
+ "noteable_iid": null,
+ "resolvable": false
}
]
}
@@ -265,7 +267,8 @@ GET /projects/:id/snippets/:snippet_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Snippet",
- "noteable_id": null
+ "noteable_id": null,
+ "resolvable": false
}
]
},
@@ -291,7 +294,8 @@ GET /projects/:id/snippets/:snippet_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Snippet",
- "noteable_id": null
+ "noteable_id": null,
+ "resolvable": false
}
]
}
@@ -409,3 +413,574 @@ Parameters:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636
```
+
+## Merge requests
+
+### List project merge request discussions
+
+Gets a list of all discussions for a single merge request.
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/discussions
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+
+```json
+[
+ {
+ "id": "6a9c1750b37d513a43987b574953fceb50b03ce7",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1126,
+ "type": "DiscussionNote",
+ "body": "discussion text",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-03T21:54:39.668Z",
+ "updated_at": "2018-03-03T21:54:39.668Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ },
+ {
+ "id": 1129,
+ "type": "DiscussionNote",
+ "body": "reply to the discussion",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T13:38:02.127Z",
+ "updated_at": "2018-03-04T13:38:02.127Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ }
+ ]
+ },
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 1128,
+ "type": null,
+ "body": "a single comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ }
+ ]
+ }
+]
+```
+
+Diff comments contain also position:
+
+```json
+[
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1128,
+ "type": DiffNote,
+ "body": "diff comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Merge request",
+ "noteable_iid": null,
+ "position": {
+ "base_sha": "b5d6e7b1613fca24d250fa8e5bc7bcc3dd6002ef",
+ "start_sha": "7c9c2ead8a320fb7ba0b4e234bd9529a2614e306",
+ "head_sha": "4803c71e6b1833ca72b8b26ef2ecd5adc8a38031",
+ "old_path": "package.json",
+ "new_path": "package.json",
+ "position_type": "text",
+ "old_line": 27,
+ "new_line": 27
+ },
+ "resolved": false,
+ "resolvable": true,
+ "resolved_by": null
+ }
+ ]
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions
+```
+
+### Get single merge request discussion
+
+Returns a single discussion for a specific project merge request
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+```
+
+### Create new merge request discussion
+
+Creates a new discussion to a single project merge request. This is similar to creating
+a note but but another comments (replies) can be added to it later.
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+| `position` | hash | no | Position when creating a diff note |
+| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
+| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
+| `position[head_sha]` | string | yes | SHA referencing HEAD of this merge request |
+| `position[position_type]` | string | yes | Type of the position reference', allowed values: 'text' or 'image' |
+| `position[new_path]` | string | no | File path after change |
+| `position[new_line]` | integer | no | Line number after change (for 'text' diff notes) |
+| `position[old_path]` | string | no | File path before change |
+| `position[old_line]` | integer | no | Line number before change (for 'text' diff notes) |
+| `position[width]` | integer | no | Width of the image (for 'image' diff notes) |
+| `position[height]` | integer | no | Height of the image (for 'image' diff notes) |
+| `position[x]` | integer | no | X coordinate (for 'image' diff notes) |
+| `position[y]` | integer | no | Y coordinate (for 'image' diff notes) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions?body=comment
+```
+
+### Resolve a merge request discussion
+
+Resolve/unresolve whole discussion of a merge request.
+
+```
+PUT /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `resolved` | boolean | yes | Resolve/unresolve the discussion |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7?resolved=true
+```
+
+
+### Add note to existing merge request discussion
+
+Adds a new note to the discussion.
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+```
+
+### Modify an existing merge request discussion note
+
+Modify or resolve an existing discussion note of a merge request.
+
+```
+PUT /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | no | The content of a discussion (exactly one of `body` or `resolved` must be set |
+| `resolved` | boolean | no | Resolve/unresolve the note (exactly one of `body` or `resolved` must be set |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment
+```
+
+Resolving a note:
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?resolved=true
+```
+
+### Delete a merge request discussion note
+
+Deletes an existing discussion note of a merge request.
+
+```
+DELETE /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/discussions/636
+```
+
+## Commits
+
+### List project commit discussions
+
+Gets a list of all discussions for a single commit.
+
+```
+GET /projects/:id/commits/:commit_id/discussions
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+
+```json
+[
+ {
+ "id": "6a9c1750b37d513a43987b574953fceb50b03ce7",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1126,
+ "type": "DiscussionNote",
+ "body": "discussion text",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-03T21:54:39.668Z",
+ "updated_at": "2018-03-03T21:54:39.668Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "resolvable": false
+ },
+ {
+ "id": 1129,
+ "type": "DiscussionNote",
+ "body": "reply to the discussion",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T13:38:02.127Z",
+ "updated_at": "2018-03-04T13:38:02.127Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "resolvable": false
+ }
+ ]
+ },
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 1128,
+ "type": null,
+ "body": "a single comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "resolvable": false
+ }
+ ]
+ }
+]
+```
+
+Diff comments contain also position:
+
+```json
+[
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1128,
+ "type": DiffNote,
+ "body": "diff comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Commit",
+ "noteable_iid": null,
+ "position": {
+ "base_sha": "b5d6e7b1613fca24d250fa8e5bc7bcc3dd6002ef",
+ "start_sha": "7c9c2ead8a320fb7ba0b4e234bd9529a2614e306",
+ "head_sha": "4803c71e6b1833ca72b8b26ef2ecd5adc8a38031",
+ "old_path": "package.json",
+ "new_path": "package.json",
+ "position_type": "text",
+ "old_line": 27,
+ "new_line": 27
+ },
+ "resolvable": false
+ }
+ ]
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions
+```
+
+### Get single commit discussion
+
+Returns a single discussion for a specific project commit
+
+```
+GET /projects/:id/commits/:commit_id/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+```
+
+### Create new commit discussion
+
+Creates a new discussion to a single project commit. This is similar to creating
+a note but but another comments (replies) can be added to it later.
+
+```
+POST /projects/:id/commits/:commit_id/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+| `position` | hash | no | Position when creating a diff note |
+| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
+| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
+| `position[head_sha]` | string | yes | SHA referencing HEAD of this commit |
+| `position[position_type]` | string | yes | Type of the position reference', allowed values: 'text' or 'image' |
+| `position[new_path]` | string | no | File path after change |
+| `position[new_line]` | integer | no | Line number after change |
+| `position[old_path]` | string | no | File path before change |
+| `position[old_line]` | integer | no | Line number before change |
+| `position[width]` | integer | no | Width of the image (for 'image' diff notes) |
+| `position[height]` | integer | no | Height of the image (for 'image' diff notes) |
+| `position[x]` | integer | no | X coordinate (for 'image' diff notes) |
+| `position[y]` | integer | no | Y coordinate (for 'image' diff notes) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions?body=comment
+```
+
+### Add note to existing commit discussion
+
+Adds a new note to the discussion.
+
+```
+POST /projects/:id/commits/:commit_id/discussions/:discussion_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+```
+
+### Modify an existing commit discussion note
+
+Modify or resolve an existing discussion note of a commit.
+
+```
+PUT /projects/:id/commits/:commit_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | no | The content of a note |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment
+```
+
+Resolving a note:
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?resolved=true
+```
+
+### Delete a commit discussion note
+
+Deletes an existing discussion note of a commit.
+
+```
+DELETE /projects/:id/commits/:commit_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `commit_id` | integer | yes | The ID of a commit |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/commits/11/discussions/636
+```
diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md
index 0d7d0fd9c42..f2353542a5c 100644
--- a/doc/api/group_badges.md
+++ b/doc/api/group_badges.md
@@ -12,7 +12,7 @@ Badges support placeholders that will be replaced in real time in both the link
- **%{default_branch}**: will be replaced by the project default branch.
- **%{commit_sha}**: will be replaced by the last project's commit sha.
-Because these enpoints aren't inside a project's context, the information used to replace the placeholders will be
+Because these endpoints aren't inside a project's context, the information used to replace the placeholders will be
from the first group's project by creation date. If the group hasn't got any project the original URL with the placeholders will be returned.
## List all badges of a group
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
index 21d3ac73000..152929b7614 100644
--- a/doc/api/group_milestones.md
+++ b/doc/api/group_milestones.md
@@ -22,7 +22,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` |
-| `state` | string | optional | Return only `active` or `closed` milestones` |
+| `state` | string | optional | Return only `active` or `closed` milestones |
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
```bash
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 1aed8aac64e..96842ef330f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -10,7 +10,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
-| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
@@ -94,7 +94,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) of the parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
-| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users) |
+| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for admin) |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
@@ -487,6 +487,9 @@ Parameters:
- `id` (required) - The ID or path of a user group
+This will queue a background job to delete all projects in the group. The
+response will be a 202 Accepted if the user has authorization.
+
## Search for group
Get all groups that match your string in their name or path.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 7479c1d2f93..5613cb6d915 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -38,8 +38,8 @@ 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 |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `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)_ |
+| `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)_ |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
@@ -152,7 +152,7 @@ GET /groups/:id/issues?my_reaction_emoji=star
| `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 |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `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)_ |
@@ -266,7 +266,7 @@ GET /projects/: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 |
| `milestone` | string | no | The milestone title |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `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)_ |
@@ -467,7 +467,7 @@ POST /projects/:id/issues
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_ids` | Array[integer] | no | The ID of the users to assign issue |
-| `milestone_id` | integer | no | The ID of a milestone to assign issue |
+| `milestone_id` | integer | no | The global ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
@@ -548,7 +548,7 @@ PUT /projects/:id/issues/:issue_iid
| `description` | string | no | The description of an issue |
| `confidential` | boolean | no | Updates an issue to be confidential |
| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
-| `milestone_id` | integer | no | The ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
+| `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
@@ -1254,3 +1254,4 @@ Example response:
[ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
[ce-17042]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042
+[ce-18935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18935
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index db4fe2f6880..e4e48edd9a7 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -82,7 +82,7 @@ Example of response
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
- "name": "spinach:other",
+ "name": "rspec:other",
"pipeline": {
"id": 6,
"ref": "master",
@@ -196,7 +196,7 @@ Example of response
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
- "name": "spinach:other",
+ "name": "rspec:other",
"pipeline": {
"id": 6,
"ref": "master",
diff --git a/doc/api/markdown.md b/doc/api/markdown.md
new file mode 100644
index 00000000000..f406838e887
--- /dev/null
+++ b/doc/api/markdown.md
@@ -0,0 +1,29 @@
+# Markdown API
+
+> [Introduced][ce-18926] in GitLab 11.0.
+
+Available only in APIv4.
+
+## Render an arbitrary Markdown document
+
+```
+POST /api/v4/markdown
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | ------------- | ------------------------------------------ |
+| `text` | string | yes | The markdown text to render |
+| `gfm` | boolean | no (optional) | Render text using GitLab Flavored Markdown. Default is `false` |
+| `project` | string | no (optional) | Use `project` as a context when creating references using GitLab Flavored Markdown. [Authentication](README.html#authentication) is required if a project is not public. |
+
+```bash
+curl --header Content-Type:application/json --data '{"text":"Hello world! :tada:", "gfm":true, "project":"group_example/project_example"}' https://gitlab.example.com/api/v4/markdown
+```
+
+Response example:
+
+```json
+{ "html": "<p dir=\"auto\">Hello world! <gl-emoji title=\"party popper\" data-name=\"tada\" data-unicode-version=\"6.0\">🎉</gl-emoji></p>" }
+```
+
+[ce-18926]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18926
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index b9a4f661777..4e34831422a 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -28,7 +28,7 @@ GET /merge_requests?milestone=release
GET /merge_requests?labels=bug,reproduced
GET /merge_requests?author_id=5
GET /merge_requests?my_reaction_emoji=star
-GET /merge_requests?scope=assigned-to-me
+GET /merge_requests?scope=assigned_to_me
```
Parameters:
@@ -45,8 +45,8 @@ Parameters:
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
-| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` |
-| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` |
+| `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)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
@@ -164,7 +164,7 @@ Parameters:
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
-| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][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)_ |
@@ -539,7 +539,7 @@ POST /projects/:id/merge_requests
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
-| `milestone_id` | integer | no | The ID of a milestone |
+| `milestone_id` | integer | no | The global ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch |
@@ -622,7 +622,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
-| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
+| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR |
| `state_event` | string | no | New state (close/reopen) |
@@ -1460,3 +1460,4 @@ Example response:
[ce-13060]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13060
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
[ce-15454]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15454
+[ce-18935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18935
diff --git a/doc/api/notes.md b/doc/api/notes.md
index aa38d22845c..d29c5b94915 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -39,7 +39,8 @@ GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
"system": true,
"noteable_id": 377,
"noteable_type": "Issue",
- "noteable_iid": 377
+ "noteable_iid": 377,
+ "resolvable": false
},
{
"id": 305,
@@ -58,7 +59,8 @@ GET /projects/:id/issues/:issue_iid/notes?sort=asc&order_by=updated_at
"system": true,
"noteable_id": 121,
"noteable_type": "Issue",
- "noteable_iid": 121
+ "noteable_iid": 121,
+ "resolvable": false
}
]
```
@@ -314,7 +316,8 @@ Parameters:
"system": false,
"noteable_id": 2,
"noteable_type": "MergeRequest",
- "noteable_iid": 2
+ "noteable_iid": 2,
+ "resolvable": false
}
```
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index c28f48e5fc6..137f1fdddec 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -108,7 +108,7 @@ POST /projects/:id/pipeline_schedules
| `description` | string | yes | The description of pipeline schedule |
| `ref` | string | yes | The branch/tag name will be triggered |
| `cron ` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
-| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
+| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) |
```sh
@@ -153,7 +153,7 @@ PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
| `description` | string | no | The description of pipeline schedule |
| `ref` | string | no | The branch/tag name will be triggered |
| `cron ` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
-| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
+| `cron_timezone ` | string | no | The timezone supported by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. |
```sh
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index a6631cab8c3..899f5da6647 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -14,6 +14,7 @@ GET /projects/:id/pipelines
| `scope` | string | no | The scope of pipelines, one of: `running`, `pending`, `finished`, `branches`, `tags` |
| `status` | string | no | The status of pipelines, one of: `running`, `pending`, `success`, `failed`, `canceled`, `skipped` |
| `ref` | string | no | The ref of pipelines |
+| `sha` | string | no | The sha or pipelines |
| `yaml_errors`| boolean | no | Returns pipelines with invalid configurations |
| `name`| string | no | The name of the user who triggered pipelines |
| `username`| string | no | The username of the user who triggered pipelines |
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index d8f61852b11..085437c801a 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -112,13 +112,13 @@ POST /projects/import
| `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project |
| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false |
-| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] |
+| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md) |
-The override params passed will take precendence over all values defined inside the export file.
+The override params passed will take precedence over all values defined inside the export file.
-To upload a file from your filesystem, use the `--form` argument. This causes
+To upload a file from your file system, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
-The `file=` parameter must point to a file on your filesystem and be preceded
+The `file=` parameter must point to a file on your file system and be preceded
by `@`. For example:
```console
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 7ffe380e275..79cf5e1cc10 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -66,6 +66,7 @@ GET /projects
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
+ "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",
"tag_list": [
"example",
"disapora client"
@@ -135,6 +136,7 @@ GET /projects
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
"web_url": "http://example.com/brightbox/puppet",
+ "readme_url": "http://example.com/brightbox/puppet/blob/master/README.md",
"tag_list": [
"example",
"puppet"
@@ -252,6 +254,7 @@ GET /users/:user_id/projects
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
+ "readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",
"tag_list": [
"example",
"disapora client"
@@ -321,6 +324,7 @@ GET /users/:user_id/projects
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
"web_url": "http://example.com/brightbox/puppet",
+ "readme_url": "http://example.com/brightbox/puppet/blob/master/README.md",
"tag_list": [
"example",
"puppet"
@@ -420,6 +424,7 @@ GET /projects/:id
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -710,6 +715,7 @@ Example responses:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -788,6 +794,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -865,6 +872,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -966,6 +974,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -1061,6 +1070,7 @@ Example response:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": "http://example.com/diaspora/diaspora-project-site/blob/master/README.md",
"tag_list": [
"example",
"disapora project"
@@ -1398,4 +1408,26 @@ Read more in the [Project Badges](project_badges.md) documentation.
## Issue and merge request description templates
-The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md). \ No newline at end of file
+The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md).
+
+## Download snapshot of a git repository
+
+> Introduced in GitLab 10.7
+
+This endpoint may only be accessed by an administrative user.
+
+Download a snapshot of the project (or wiki, if requested) git repository. This
+snapshot is always in uncompressed [tar](https://en.wikipedia.org/wiki/Tar_(computing))
+format.
+
+If a repository is corrupted to the point where `git clone` does not work, the
+snapshot may allow some of the data to be retrieved.
+
+```
+GET /projects/:id/snapshot
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
diff --git a/doc/api/runners.md b/doc/api/runners.md
index f384ac57bfe..3ca07ce9795 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -411,3 +411,86 @@ DELETE /projects/:id/runners/:runner_id
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners/9"
```
+
+## Register a new Runner
+
+Register a new Runner for the instance.
+
+```
+POST /runners
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Registration token ([Read how to obtain a token](../ci/runners/README.md)) |
+| `description`| string | no | Runner's description|
+| `info` | hash | no | Runner's metadata |
+| `active` | boolean| no | Whether the Runner is active |
+| `locked` | boolean| no | Whether the Runner should be locked for current project |
+| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs |
+| `tag_list` | Array[String] | no | List of Runner's tags |
+| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
+
+```
+curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=ipzXrMhuyyJPifUt6ANz" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 201 | Runner was created |
+
+Example response:
+
+```json
+{
+ "id": "12345",
+ "token": "6337ff461c94fd3fa32ba3b1ff4125"
+}
+```
+
+## Delete a registered Runner
+
+Deletes a registed Runner.
+
+```
+DELETE /runners
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Runner's authentication token |
+
+```
+curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=ebb6fc00521627750c8bb750f2490e"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 204 | Runner was deleted |
+
+## Verify authentication for a registered Runner
+
+Validates authentication credentials for a registered Runner.
+
+```
+POST /runners/verify
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|---------------------|
+| `token` | string | yes | Runner's authentication token |
+
+```
+curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=ebb6fc00521627750c8bb750f2490e"
+```
+
+Response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 200 | Credentials are valid |
+| 403 | Credentials are invalid |
diff --git a/doc/api/services.md b/doc/api/services.md
index 92f12acbc73..f23303ef836 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -405,6 +405,13 @@ GET /projects/:id/services/flowdock
Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.
+CAUTION: **Warning:**
+Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been
+[acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html)
+in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available.
+You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html)
+to keep monitoring your dependencies.
+
### Create/Edit Gemnasium service
Set Gemnasium service for a project.
@@ -968,7 +975,7 @@ Group Chat Software
Set Microsoft Teams service for a project.
```
-PUT /projects/:id/services/microsoft_teams
+PUT /projects/:id/services/microsoft-teams
```
Parameters:
@@ -982,7 +989,7 @@ Parameters:
Delete Microsoft Teams service for a project.
```
-DELETE /projects/:id/services/microsoft_teams
+DELETE /projects/:id/services/microsoft-teams
```
### Get Microsoft Teams service settings
@@ -990,7 +997,7 @@ DELETE /projects/:id/services/microsoft_teams
Get Microsoft Teams service settings for a project.
```
-GET /projects/:id/services/microsoft_teams
+GET /projects/:id/services/microsoft-teams
```
## Mattermost notifications
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 0b5b1f0c134..e06b1bfb6df 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -53,6 +53,8 @@ Example response:
"dsa_key_restriction": 0,
"ecdsa_key_restriction": 0,
"ed25519_key_restriction": 0,
+ "enforce_terms": true,
+ "terms": "Hello world!",
}
```
@@ -153,6 +155,8 @@ PUT /application/settings
| `user_default_external` | boolean | no | Newly registered users will by default be external |
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
+| `enforce_terms` | boolean | no | Enforce application ToS to all users |
+| `terms` | text | yes (if `enforce_terms` is true) | Markdown content for the ToS |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
@@ -195,5 +199,7 @@ Example response:
"dsa_key_restriction": 0,
"ecdsa_key_restriction": 0,
"ed25519_key_restriction": 0,
+ "enforce_terms": true,
+ "terms": "Hello world!",
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index a4447e32908..ca5afa04687 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -55,6 +55,7 @@ GET /users
| --------- | ---- | -------- | ----------- |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
```json
[
diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md
index b7594cfef7f..76fdb2eb00a 100644
--- a/doc/articles/openshift_and_gitlab/index.md
+++ b/doc/articles/openshift_and_gitlab/index.md
@@ -1 +1,4 @@
-This document was moved to [another location](../../install/openshift_and_gitlab/index.html).
+---
+redirect_to: '../../install/openshift_and_gitlab/index.html'
+---
+
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 6aa0e5885db..8d1d72c2a2b 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Integration, Continuous Deployment, and Continuous Delivery toolset to build, test, and deploy your application."
---
# GitLab Continuous Integration (GitLab CI/CD)
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index b3d9f0bc96c..3a491f0073c 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -252,6 +252,7 @@ including predefined, secure variables and `.gitlab-ci.yml`
[`variables`](yaml/README.md#variables). You however cannot use variables
defined under `script` or on the Runner's side. There are other variables that
are unsupported in environment name context:
+- `CI_PIPELINE_ID`
- `CI_JOB_ID`
- `CI_JOB_TOKEN`
- `CI_BUILD_ID`
@@ -260,6 +261,8 @@ are unsupported in environment name context:
- `CI_REGISTRY_PASSWORD`
- `CI_REPOSITORY_URL`
- `CI_ENVIRONMENT_URL`
+- `CI_DEPLOY_USER`
+- `CI_DEPLOY_PASSWORD`
GitLab Runner exposes various [environment variables][variables] when a job runs,
and as such, you can use them as environment names. Let's add another job in
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index 92317c77427..cc19e090964 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -5,10 +5,10 @@ GitLab CI and Docker.
First, you need GitLab Runner with [docker-in-docker executor][dind].
-Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codequality`:
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `code_quality`:
```yaml
-codequality:
+code_quality:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -17,22 +17,33 @@ codequality:
- 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
+ - 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: [codeclimate.json]
+ paths: [gl-code-quality-report.json]
```
-The above example will create a `codequality` job in your CI/CD pipeline which
+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.
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 `codequality` and the artifact path must be
-`codeclimate.json`.
+so, the CI/CD job must be named `code_quality` and the artifact path must be
+`gl-code-quality-report.json`.
[Learn more on code quality diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
+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.
+
[cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[ee]: https://about.gitlab.com/products/
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index dc34f4acd75..92ff90507ee 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -7,10 +7,10 @@ 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 `sast:container`:
+called `container_scanning`:
```yaml
-sast:container:
+container_scanning:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -23,7 +23,7 @@ sast:container:
- docker:stable-dind
script:
- docker run -d --name db arminc/clair-db:latest
- - docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1
+ - 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
@@ -31,12 +31,15 @@ sast:container:
- chmod +x clair-scanner
- touch clair-whitelist.yml
- while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
- - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
+ - 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-sast-container-report.json]
+ paths: [gl-container-scanning-report.json]
```
-The above example will create a `sast:container` job in your CI/CD pipeline, pull
+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
@@ -49,8 +52,15 @@ 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 `sast:container` and the artifact path must be
-`gl-sast-container-report.json`.
+so, the CI/CD job must be named `container_scanning` and the artifact path must be
+`gl-container-scanning-report.json`.
[Learn more on container scanning results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html).
+CAUTION: **Caution:**
+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.
+
[ee]: https://about.gitlab.com/products/
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index 8df223ee560..a8720f0b7ea 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -42,9 +42,9 @@ dast:
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" \
+ - /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
- cp /zap/wrk/gl-dast-report.json .
artifacts:
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 bfc8558a580..3d21c0cc306 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
@@ -509,7 +509,7 @@ and unit tests, all running and deployed at every push to master - with shocking
Errors can be easily debugged through GitLab's build logs, and within minutes of a successful commit,
you can see the changes live on your game.
-Setting up Continous Integration and Continuous Deployment from the start with Dark Nova enables
+Setting up Continuous Integration and Continuous Deployment from the start with Dark Nova enables
rapid but stable development. We can easily test changes in a separate [environment](../../../ci/environments.md#introduction-to-environments-and-deployments),
or multiple environments if needed. Balancing and updating a multiplayer game can be ongoing
and tedious, but having faith in a stable deployment with GitLab CI/CD allows
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 7f6519fd38e..a2de0408797 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
@@ -30,7 +30,7 @@ and GitLab UI._
Many components and concepts are similar to Ruby on Rails or Python's Django. High developer
productivity and high application performance are only a few advantages on learning how to use it.
-Working on the MVC pattern, it's was designed to be modular and flexible. Easy to mantain a growing
+Working on the MVC pattern, it's was designed to be modular and flexible. Easy to maintain a growing
app is a plus.
Phoenix can run in any OS where Erlang is supported:
@@ -48,7 +48,7 @@ Check the [Phoenix learning guide][phoenix-learning-guide] for more information.
### What is Elixir?
[Elixir][elixir-site] is a dynamic, functional language created to use all the maturity of Erlang
-(30 years old!) in these days, in an easy way. It has similarities with Ruby, specially on sintax,
+(30 years old!) in these days, in an easy way. It has similarities with Ruby, specially on syntax,
so Ruby developers are quite excited with the rapid growing of Elixir. A full-stack Ruby developer
can learn how to use Elixir and Phoenix in just a few weeks!
@@ -162,7 +162,7 @@ productive, because every time we, or our co-workers push any code, GitLab CI/CD
test the changes, telling us in realtime if anything goes wrong.
Certainly, when our application starts to grow, we'll need more developers working on the same
-project and this process of building and testing can easely become a mess without proper management.
+project and this process of building and testing can easily become a mess without proper management.
That's also why GitLab CI/CD is so important to our application. Every time someone pushes its code to
GitLab, we'll quickly know if their changes broke something or not. We don't need to stop everything
we're doing to test manually and locally every change our team does.
@@ -237,7 +237,7 @@ Finished in 0.7 seconds
Randomized with seed 610000
```
-Our test was successfull. It's time to push our files to GitLab.
+Our test was successful. It's time to push our files to GitLab.
## Configuring CI/CD Pipeline
@@ -302,7 +302,7 @@ template** and select **Elixir**:
```
It's important to install `postgresql-client` to let GitLab CI/CD access PostgreSQL and create our
- database with the login information provided earlier. More important is to respect the identation,
+ database with the login information provided earlier. More important is to respect the indentation,
to avoid syntax errors when running the build.
- And finally, we'll let `mix` session intact.
@@ -333,7 +333,7 @@ mix:
- mix test
```
-For safety, we can check if we get any syntax errors before submiting this file to GitLab. Copy the
+For safety, we can check if we get any syntax errors before submitting this file to GitLab. Copy the
contents of `.gitlab-ci.yml` and paste it on [GitLab CI/CD Lint tool][ci-lint]. Please note that
this link will only work for logged in users.
@@ -384,7 +384,7 @@ working properly.
When we have a growing application with many developers working on it, or when we have an open
source project being watched and contributed by the community, it is really important to have our
-code permanently working. GitLab CI/CD is a time saving powerfull tool to help us mantain our code
+code permanently working. GitLab CI/CD is a time saving powerful tool to help us maintain our code
organized and working.
As we could see in this post, GitLab CI/CD is really really easy to configure and use. We have [many
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index f14e0527998..b16cbc61d14 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -75,7 +75,7 @@ cancel the job, retry it, or erase the job trace.
## Seeing the failure reason for jobs
-> [Introduced][ce-5742] in GitLab 10.7.
+> [Introduced][ce-17782] in GitLab 10.7.
When a pipeline fails or is allowed to fail, there are several places where you
can quickly check the reason it failed:
@@ -88,6 +88,8 @@ In any case, if you hover over the failed job you can see the reason it failed.
![Pipeline detail](img/job_failure_reason.png)
+From [GitLab 10.8][ce-17814] you can also see the reason it failed on the Job detail page.
+
## Pipeline graphs
> [Introduced][ce-5742] in GitLab 8.11.
@@ -279,4 +281,5 @@ runners will not use regular runners, they must be tagged accordingly.
[ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
[ce-17782]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
+[ce-17814]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814
[regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index fec0ff87326..47e658f610e 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -151,7 +151,8 @@ The next step is to configure a Runner so that it picks the pending jobs.
In GitLab, Runners run the jobs that you define in `.gitlab-ci.yml`. A Runner
can be a virtual machine, a VPS, a bare-metal machine, a docker container or
even a cluster of containers. GitLab and the Runners communicate through an API,
-so the only requirement is that the Runner's machine has [Internet] access.
+so the only requirement is that the Runner's machine has network access to the
+GitLab server.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
@@ -226,4 +227,3 @@ CI with various languages.
[enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages
[pipeline]: ../pipelines.md
-[internet]: https://about.gitlab.com/images/theinternet.png
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 60dc2ef9ac5..703a7f030ed 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -11,7 +11,7 @@ Ideally, the GitLab Runner should not be installed on the same machine as GitLab
Read the [requirements documentation](../../install/requirements.md#gitlab-runner)
for more information.
-## Shared vs specific Runners
+## Shared, specific and group Runners
After [installing the Runner][install], you can either register it as shared or
specific. You can only register a shared Runner if you have admin access to
@@ -32,6 +32,9 @@ are:
Runners. For example, if you want to deploy a certain project, you can setup
a specific Runner to have the right credentials for this. The [usage of tags](#using-tags)
may be useful in this case. Specific Runners process jobs using a [FIFO] queue.
+- **Group Runners** are useful when you have multiple projects under one group
+ and would like all projects to have access to a set of Runners. Group Runners
+ process jobs using a [FIFO] queue.
A Runner that is specific only runs for the specified project(s). A shared Runner
can run jobs for every project that has enabled the option **Allow shared Runners**
@@ -66,7 +69,7 @@ Runners to disabled.
## Registering a specific Runner
-Registering a specific can be done in two ways:
+Registering a specific Runner can be done in two ways:
1. Creating a Runner with the project registration token
1. Converting a shared Runner into a specific Runner (one-way, admin only)
@@ -79,6 +82,14 @@ visit the project you want to make the Runner work for in GitLab:
1. Go to **Settings > CI/CD** to obtain the token
1. [Register the Runner][register]
+## Registering a group Runner
+
+Creating a group Runner requires Master permissions for the group. To create a
+group Runner visit the group you want to make the Runner work for in GitLab:
+
+1. Go to **Settings > CI/CD** to obtain the token
+1. [Register the Runner][register]
+
### Making an existing shared Runner specific
If you are an admin on your GitLab instance, you can turn any shared Runner into
@@ -121,7 +132,7 @@ To enable/disable a Runner in your project:
> **Note**:
Consider that if you don't lock your specific Runner to a specific project, any
-user with Master role in you project can assign your runner to another arbitrary
+user with Master role in you project can assign your Runner to another arbitrary
project without requiring your authorization, so use it with caution.
An admin can enable/disable a specific Runner for projects:
@@ -298,6 +309,28 @@ Mentioned briefly earlier, but the following things of Runners can be exploited.
We're always looking for contributions that can mitigate these
[Security Considerations](https://docs.gitlab.com/runner/security/).
+### Resetting the registration token for a Project
+
+If you think that registration token for a Project was revealed, you should
+reset them. It's recommended because such token can be used to register another
+Runner to thi Project. It may be next used to obtain the values of secret
+variables or clone the project code, that normally may be unavailable for the
+attacker.
+
+To reset the token:
+
+1. Go to **Settings > CI/CD** for a specified Project
+1. Expand the **General pipelines settings** section
+1. Find the **Runner token** form field and click the **Reveal value** button
+1. Delete the value and save the form
+1. After the page is refreshed, expand the **Runners settings** section
+ and check the registration token - it should be changed
+
+From now on the old token is not valid anymore and will not allow to register
+a new Runner to the project. If you are using any tools to provision and
+register new Runners, you should now update the token that is used to the
+new value.
+
## Determining the IP address of a Runner
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
diff --git a/doc/ci/services/docker-services.md b/doc/ci/services/docker-services.md
index 787c5e462e4..e5fc7a3c85f 100644
--- a/doc/ci/services/docker-services.md
+++ b/doc/ci/services/docker-services.md
@@ -1,9 +1,3 @@
---
-comments: false
+redirect_to: 'README.md'
---
-
-# GitLab CI Services
-
-- [Using MySQL](mysql.md)
-- [Using PostgreSQL](postgres.md)
-- [Using Redis](redis.md)
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 4a504a98902..aedf7958c8a 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -41,6 +41,9 @@ future GitLab releases.**
| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
+| **CI_COMMIT_MESSAGE** | 10.8 | all | The full commit message. |
+| **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message |
+| **CI_COMMIT_DESCRIPTION** | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
| **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
@@ -61,7 +64,7 @@ future GitLab releases.**
| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
-| **CI_PIPELINE_SOURCE** | 10.0 | all | The source for this pipeline, one of: push, web, trigger, schedule, api, external. Pipelines created before 9.5 will have unknown as source |
+| **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
@@ -87,6 +90,8 @@ future GitLab releases.**
| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
+| **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
+| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
## 9.0 Renaming
@@ -525,6 +530,16 @@ Below you can find supported syntax reference:
`$STAGING` value needs to a string, with length higher than zero.
Variable that contains only whitespace characters is not an empty variable.
+1. Pattern matching _(added in 11.0)_
+
+ > Example: `$VARIABLE =~ /^content.*/`
+
+ It is possible perform pattern matching against a variable and regular
+ expression. Expression like this evaluates to truth if matches are found.
+
+ Pattern matching is case-sensitive by default. Use `i` flag modifier, like
+ `/pattern/i` to make a pattern case-insensitive.
+
### Unsupported predefined variables
Because GitLab evaluates variables before creating jobs, we do not support a
@@ -538,6 +553,7 @@ We do not support variables containing tokens because of security reasons.
You can find a full list of unsupported variables below:
+- `CI_PIPELINE_ID`
- `CI_JOB_ID`
- `CI_JOB_TOKEN`
- `CI_BUILD_ID`
@@ -546,8 +562,10 @@ You can find a full list of unsupported variables below:
- `CI_REGISTRY_PASSWORD`
- `CI_REPOSITORY_URL`
- `CI_ENVIRONMENT_URL`
+- `CI_DEPLOY_USER`
+- `CI_DEPLOY_PASSWORD`
-These variables are also not supported in a contex of a
+These variables are also not supported in a context of a
[dynamic environment name][dynamic-environments].
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables"
@@ -562,3 +580,4 @@ These variables are also not supported in a contex of a
[subgroups]: ../../user/group/subgroups/index.md
[builds-policies]: ../yaml/README.md#only-and-except-complex
[dynamic-environments]: ../environments.md#dynamic-environments
+[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 623e7d662a3..3e77a6f58b7 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -344,10 +344,11 @@ job:
kubernetes: active
```
-Example of using variables expressions:
+Examples of using variables expressions:
```yaml
deploy:
+ script: cap staging deploy
only:
refs:
- branches
@@ -356,6 +357,16 @@ deploy:
- $STAGING
```
+Another use case is exluding jobs depending on a commit message _(added in 11.0)_:
+
+```yaml
+end-to-end:
+ script: rake test:end-to-end
+ except:
+ variables:
+ - $CI_COMMIT_MESSAGE =~ /skip-end-to-end-tests/
+```
+
Learn more about variables expressions on [a separate page][variables-expressions].
## `tags`
@@ -738,10 +749,15 @@ cache:
rspec:
script: test
cache:
+ key: rspec
paths:
- binaries/
```
+Note that since cache is shared between jobs, if you're using different
+paths for different jobs, you should also set a different **cache:key**
+otherwise cache content can be overwritten.
+
### `cache:key`
> Introduced in GitLab Runner v1.0.0.
@@ -756,10 +772,9 @@ or any other way that fits your workflow. This way, you can fine tune caching,
allowing you to cache data between different jobs or even different branches.
The `cache:key` variable can use any of the
-[predefined variables](../variables/README.md), and the default key, if not set,
-is `$CI_JOB_NAME-$CI_COMMIT_REF_NAME` which translates as "per-job and
-per-branch". It is the default across the project, therefore everything is
-shared between pipelines and jobs running on the same branch by default.
+[predefined variables](../variables/README.md), and the default key, if not
+set, is just literal `default` which means everything is shared between each
+pipelines and jobs by default, starting from GitLab 9.0.
NOTE: **Note:**
The `cache:key` variable cannot contain the `/` character, or the equivalent
@@ -779,7 +794,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
cache:
- key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_SLUG%"
+ key: "%CI_COMMIT_REF_SLUG%"
paths:
- binaries/
```
@@ -789,7 +804,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace
```yaml
cache:
- key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_SLUG"
+ key: "$env:CI_COMMIT_REF_SLUG"
paths:
- binaries/
```
@@ -1572,8 +1587,8 @@ capitalization, the commit will be created but the pipeline will be skipped.
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint, which validates the
-content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
-project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/ci/lint`)
+content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
+project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/-/ci/lint`)
## Using reserved keywords
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index fe3e4681ba7..5d2f5edcb18 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1 +1 @@
-This document was moved in [user/project/container_registry](../user/project/container_registry.md).
+This document was moved to [another location](../user/project/container_registry.md).
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index d14ba6ad522..680c51e7524 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,8 +1,3 @@
---
-comments: false
+redirect_to: '../user/project/issues/automatic_issue_closing.md'
---
-
-This document was split into:
-
-- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md).
-- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/development/README.md b/doc/development/README.md
index 45e9565f9a7..898c60e96c0 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: 'Learn how to contribute to GitLab.'
---
# GitLab development guides
@@ -18,6 +19,7 @@ comments: false
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Automatic CE->EE merge](automatic_ce_ee_merge.md)
- [Guidelines for implementing Enterprise Edition features](ee_features.md)
+- [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer)
## UX and frontend guides
@@ -39,9 +41,9 @@ comments: false
- [Sidekiq debugging](sidekiq_debugging.md)
- [Gotchas](gotchas.md) to avoid
- [Avoid modules with instance variables](module_with_instance_variables.md) if possible
-- [Issue and merge requests state models](object_state_models.md)
- [How to dump production data to staging](db_dump.md)
- [Working with the GitHub importer](github_importer.md)
+- [Working with Merge Request diffs](diffs.md)
## Performance guides
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index ce69694ab6a..46c5baddb9c 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -24,7 +24,7 @@ Some examples where background migrations can be useful:
* Migrating events from one table to multiple separate tables.
* Populating one column based on JSON stored in another column.
-* Migrating data that depends on the output of exernal services (e.g. an API).
+* Migrating data that depends on the output of external services (e.g. an API).
## Isolation
@@ -46,7 +46,7 @@ See [Sidekiq best practices guidelines](https://github.com/mperham/sidekiq/wiki/
for more details.
Make sure that in case that your migration job is going to be retried data
-integrity is guarateed.
+integrity is guaranteed.
## How It Works
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index d5a4acff481..a9fa5ae834f 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community
contributors. **Both are optional**.
The `type` field maps the category of the change,
-valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**.
+valid options are: added, fixed, changed, deprecated, removed, security, performance, other. **Type field is mandatory**.
Community contributors and core team members are encouraged to add their name to
the `author` field. GitLab team members **should not**.
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 7165b8062a7..d03b7fa23ca 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -29,6 +29,10 @@ There are a few rules to get your merge request accepted:
to ask one of the [Merge request coaches][team].
1. The reviewer will assign the merge request to a maintainer once the
reviewer is satisfied with the state of the merge request.
+1. Keep in mind that maintainers are also going to perform a final code review.
+ The ideal scenario is that the reviewer has already addressed any concerns
+ the maintainer would have found, and the maintainer only has to perform the
+ merge, but be prepared for further review comments.
For more guidance, see [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
@@ -207,3 +211,4 @@ Largely based on the [thoughtbot code review guide].
[projects]: https://about.gitlab.com/handbook/engineering/projects/
[team]: https://about.gitlab.com/team/
[build handbook]: https://about.gitlab.com/handbook/build/handbook/build#how-to-work-with-build
+[^1]: Please note that specs other than JavaScript specs are considered backend code.
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
index 32f392f1303..9c31265e417 100644
--- a/doc/development/database_debugging.md
+++ b/doc/development/database_debugging.md
@@ -11,7 +11,7 @@ Available `RAILS_ENV`
- `production` (generally not for your main GDK db, but you may need this for e.g. omnibus)
- `development` (this is your main GDK db)
- - `test` (used for tests like rspec and spinach)
+ - `test` (used for tests like rspec)
## Nuke everything and start over
diff --git a/doc/development/diffs.md b/doc/development/diffs.md
new file mode 100644
index 00000000000..55fc16e0b33
--- /dev/null
+++ b/doc/development/diffs.md
@@ -0,0 +1,115 @@
+# Working with Merge Request diffs
+
+Currently we rely on different sources to present merge request diffs, these include:
+
+- Rugged gem
+- Gitaly service
+- Database (through `merge_request_diff_files`)
+- Redis (cached highlighted diffs)
+
+We're constantly moving Rugged calls to Gitaly and the progress can be followed through [Gitaly repo](https://gitlab.com/gitlab-org/gitaly).
+
+## Architecture overview
+
+When refreshing a Merge Request (pushing to a source branch, force-pushing to target branch, or if the target branch now contains any commits from the MR)
+we fetch the comparison information using `Gitlab::Git::Compare`, which fetches `base` and `head` data using Gitaly and diff between them through
+`Gitlab::Git::Diff.between` (which uses _Gitaly_ if it's enabled, otherwise _Rugged_).
+The diffs fetching process _limits_ single file diff sizes and the overall size of the whole diff through a series of constant values. Raw diff files are
+then persisted on `merge_request_diff_files` table.
+
+Even though diffs higher than 10kb are collapsed (`Gitlab::Git::Diff::COLLAPSE_LIMIT`), we still keep them on Postgres. However, diff files over _safety limits_
+(see the [Diff limits section](#diff-limits)) are _not_ persisted.
+
+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`
+
+## Diff limits
+
+As explained above, we limit single diff files and the size of the whole diff. There are scenarios where we collapse the diff file,
+and cases where the diff file is not presented at all, and the user is guided to the Blob view. Here we'll go into details about
+these limits.
+
+### Diff collection limits
+
+Limits that act onto all diff files collection. Files number, lines number and files size are considered.
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_files] = 100
+```
+
+File diffs will be collapsed (but be expandable) if 100 files have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:safe_max_lines] = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000
+```
+
+File diffs will be collapsed (but be expandable) if 5000 lines have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:safe_max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:safe_max_files] * 5.kilobytes = 500.kilobytes
+```
+
+File diffs will be collapsed (but be expandable) if 500 kilobytes have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:max_files] = Commit::DIFF_HARD_LIMIT_FILES = 1000
+```
+
+No more files will be rendered at all if 1000 files have already been rendered.
+
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:max_lines] = Commit::DIFF_HARD_LIMIT_LINES = 50000
+```
+
+No more files will be rendered at all if 50,000 lines have already been rendered.
+
+```ruby
+Gitlab::Git::DiffCollection.collection_limits[:max_bytes] = Gitlab::Git::DiffCollection.collection_limits[:max_files] * 5.kilobytes = 5000.kilobytes
+```
+
+No more files will be rendered at all if 5 megabytes have already been rendered.
+
+
+### Individual diff file limits
+
+Limits that act onto each diff file of a collection. Files number, lines number and files size are considered.
+
+```ruby
+Gitlab::Git::Diff::COLLAPSE_LIMIT = 10.kilobytes
+```
+
+File diff will be collapsed (but be expandable) if it is larger than 10 kilobytes.
+
+```ruby
+Gitlab::Git::Diff::SIZE_LIMIT = 100.kilobytes
+```
+
+File diff will not be rendered if it's larger than 100 kilobytes.
+
+
+```ruby
+Commit::DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] = 5000
+```
+
+File diff will be suppressed (technically different from collapsed, but behaves the same, and is expandable) if it has more than 5000 lines.
+
+## Viewers
+
+Diff Viewers, which can be found on `models/diff_viewer/*` are classes used to map metadata about each type of Diff File. It has information
+whether it's a binary, which partial should be used to render it or which File extensions this class accounts for.
+
+`DiffViewer::Base` validates _blobs_ (old and new versions) content, extension and file type in order to check if it can be rendered.
+
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 0550ea527cb..5d595c33915 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -1,10 +1,14 @@
+---
+description: 'Writing styles, markup, formatting, and reusing regular expressions throughout the GitLab Documentation.'
+---
+
# Documentation style guidelines
The documentation style guide defines the markup structure used in
GitLab documentation. Check the
[documentation guidelines](writing_documentation.md) for general development instructions.
-Check the GitLab hanbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
+Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
## Text
@@ -19,25 +23,41 @@ Check the GitLab hanbook for the [writing styles guidelines](https://about.gitla
- Unless there's a logical reason not to, add documents in alphabetical order
- Write in US English
- Use [single spaces][] instead of double spaces
-- Jump a line between different markups (e.g., after every paragraph, hearder, list, etc)
+- Jump a line between different markups (e.g., after every paragraph, header, list, etc)
- Capitalize "G" and "L" in GitLab
-- Capitalize feature, products, and methods names. E.g.: GitLab Runner, Geo,
-Issue Boards, Git, Prometheus, Continuous Integration.
+- 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/), and methods. Note that some features are also objects (e.g. "Merge Requests" and "merge requests"). E.g.: GitLab Runner, Geo, Issue Boards, Git, Prometheus, Continuous Integration.
## Formatting
+- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`)
+- Use undescore (`_`) for text in italics (`_italic_`)
+- Jump a line between different markups, for example:
+
+ ```md
+ ## Header
+
+ Paragraph.
+
+ - List item
+ - List item
+ ```
+
+### Punctuation
+
+For punctuation rules, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/).
+
+### Ordered and unordered lists
+
- Use dashes (`-`) for unordered lists instead of asterisks (`*`)
- Use the number one (`1`) for ordered lists
-- Use underscores (`_`) to mark a word or text in italics
-- Use double asterisks (`**`) to mark a word or text in bold
-- When using lists, prefer not to end each item with a period. You can use
- them if there are multiple sentences, just keep the last sentence without
- a period
+- For punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/)
## Headings
-- Add only one H1 title in each document, by adding `#` at the beginning of
- it (when using markdown). For subheadings, use `##`, `###` and so on
+- Add **only one H1** in each document, by adding `#` at the beginning of
+ it (when using markdown). The `h1` will be the document `<title>`.
+- For subheadings, use `##`, `###` and so on
- 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
@@ -106,21 +126,75 @@ Inside the document:
- If a heading is placed right after an image, always add three dashes (`---`)
between the image and the heading
-## Notes
+## Alert boxes
-- Notes should be quoted with the word `Note:` being bold. Use this form:
+Whenever you want to call the attention to a particular sentence,
+use the following markup for highlighting.
- ```md
- >**Note:**
- This is something to note.
- ```
+_Note that the alert boxes only work for one paragraph only. Multiple paragraphs,
+lists, headers, etc will not render correctly._
+
+### Note
+
+```md
+NOTE: **Note:**
+This is something to note.
+```
+
+How it renders in docs.gitlab.com:
+
+NOTE: **Note:**
+This is something to note.
+
+### Tip
+
+```md
+TIP: **Tip:**
+This is a tip.
+```
+
+How it renders in docs.gitlab.com:
+
+TIP: **Tip:**
+This is a tip.
+
+### Caution
+
+```md
+CAUTION: **Caution:**
+This is something to be cautious about.
+```
+
+How it renders in docs.gitlab.com:
+
+CAUTION: **Caution:**
+This is something to be cautious about.
+
+### Danger
- which renders to:
+```md
+DANGER: **Danger:**
+This is a breaking change, a bug, or something very important to note.
+```
+
+How it renders in docs.gitlab.com:
- >**Note:**
- This is something to note.
+DANGER: **Danger:**
+This is a breaking change, a bug, or something very important to note.
- If the note spans across multiple lines it's OK to split the line.
+## Blockquotes
+
+For highlighting a text within a blue blockquote, use this format:
+
+```md
+> This is a blockquote.
+```
+
+which renders in docs.gitlab.com to:
+
+> This is a blockquote.
+
+If the text spans across multiple lines it's OK to split the line.
## Specific sections and terms
@@ -137,7 +211,7 @@ below.
> Introduced in GitLab 8.3.
```
-- If possible every feature should have a link to the MR, issue, or epic that introduced it.
+- Whenever possible, every feature should have a link to the MR, issue, or epic that introduced it.
The above note would be then transformed to:
```md
@@ -152,11 +226,9 @@ below.
the feature is available in:
```md
- > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/products/) 8.3.
+ > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
```
- Otherwise, leave this mention out.
-
### Product badges
When a feature is available in EE-only tiers, add the corresponding tier according to the
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 287143d6255..057a4094aed 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -53,6 +53,9 @@ stub_licensed_features(variable_environment_scope: true)
EE-specific comments should not be backported to CE.
+**Note:** This is only meant as a workaround, we should follow up and
+resolve this soon.
+
### Detection of EE-only files
For each commit (except on `master`), the `ee-files-location-check` CI job tries
@@ -105,11 +108,14 @@ is applied not only to models. Here's a list of other examples:
- `ee/app/services/foo/create_service.rb`
- `ee/app/validators/foo_attr_validator.rb`
- `ee/app/workers/foo_worker.rb`
+- `ee/app/views/foo.html.haml`
+- `ee/app/views/foo/_bar.html.haml`
This works because for every path that are present in CE's eager-load/auto-load
paths, we add the same `ee/`-prepended path in [`config/application.rb`].
+This also applies to views.
-[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/d278b76d6600a0e27d8019a0be27971ba23ab640/config/application.rb#L41-51
+[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/925d3d4ebc7a2c72964ce97623ae41b8af12538d/config/application.rb#L42-52
### EE features based on CE features
@@ -279,7 +285,7 @@ end
```
In `lib/gitlab/visibility_level.rb` this method is used to return the
-allowed visibilty levels:
+allowed visibility levels:
```ruby
def levels_for_user(user = nil)
@@ -359,9 +365,37 @@ Blocks of code that are EE-specific should be moved to partials. This
avoids conflicts with big chunks of HAML code that that are not fun to
resolve when you add the indentation to the equation.
-EE-specific views should be placed in `ee/app/views/ee/`, using extra
+EE-specific views should be placed in `ee/app/views/`, using extra
sub-directories if appropriate.
+Instead of using regular `render`, we should use `render_if_exists`, which
+will not render anything if it cannot find the specific partial. We use this
+so that we could put `render_if_exists` in CE, keeping code the same between
+CE and EE.
+
+Also, it should search for the EE partial first, and then CE partial, and
+then if nothing found, render nothing.
+
+This has two uses:
+
+- CE renders nothing, and EE renders its EE partial.
+- CE renders its CE partial, and EE renders its EE partial, while the view
+ file stays the same.
+
+The advantages of this:
+
+- Minimal code difference between CE and EE.
+- Very clear hints about where we're extending EE views while reading CE codes.
+- Whenever we want to show something different in CE, we could just add CE
+ partials. Same applies the other way around. If we just use
+ `render_if_exists`, it would be very easy to change the content in EE.
+
+The disadvantage of this:
+
+- Slightly more work while developing EE features, because now we need to
+ port `render_if_exists` to CE.
+- If we have typos in the partial name, it would be silently ignored.
+
### Code in `lib/`
Place EE-specific logic in the top-level `EE` module namespace. Namespace the
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
new file mode 100644
index 00000000000..d240dbe8b02
--- /dev/null
+++ b/doc/development/fe_guide/development_process.md
@@ -0,0 +1,77 @@
+# Frontend Development Process
+
+You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/frontend/).
+
+## Development Checklist
+
+The idea is to remind us about specific topics during the time we build a new feature or start something. This is a common practice in other industries (like pilots) that also use standardised checklists to reduce problems early on.
+
+Copy the content over to your issue or merge request and if something doesn't apply simply remove it from your current list.
+
+This checklist is intended to help us during development of bigger features/refactorings, it's not a "use it always and every point always matches" list.
+
+Please use your best judgement when to use it and please contribute new points through merge requests if something comes to your mind.
+
+---
+
+### Frontend development
+
+#### Planning development
+
+- [ ] Check the current set weight of the issue, does it fit your estimate?
+- [ ] Are all [departments](https://about.gitlab.com/handbook/engineering/#engineering-teams) that are needed from your perspective already involved in the issue? (For example is UX missing?)
+- [ ] Is the specification complete? Are you missing decisions? How about error handling/defaults/edge cases? Take your time to understand the needed implementation and go through its flow.
+- [ ] Are all necessary UX specifications available that you will need in order to implement? Are there new UX components/patterns in the designs? Then contact the UI component team early on. How should error messages or validation be handled?
+- [ ] **Library usage** Use Vuex as soon as you have even a medium state to manage, use Vue router if you need to have different views internally and want to link from the outside. Check what libraries we already have for which occasions.
+- [ ] **Plan your implementation:**
+ - [ ] **Architecture plan:** Create a plan aligned with GitLab's architecture, how you are going to do the implementation, for example Vue application setup and its components (through [onion skinning](https://gitlab.com/gitlab-org/gitlab-ce/issues/35873#note_39994091)), Store structure and data flow, which existing Vue components can you reuse. It's a good idea to go through your plan with another engineer to refine it.
+ - [ ] **Backend:** The best way is to kickoff the implementation in a call and discuss with the assigned Backend engineer what you will need from the backend and also when. Can you reuse existing API's? How is the performance with the planned architecture? Maybe create together a JSON mock object to already start with development.
+ - [ ] **Communication:** It also makes sense to have for bigger features an own slack channel (normally called #f_{feature_name}) and even weekly demo calls with all people involved.
+ - [ ] **Dependency Plan:** Are there big dependencies in the plan between you and others, then maybe create an execution diagram to show what is blocking which part and the order of the different parts.
+ - [ ] **Task list:** Create a simple checklist of the subtasks that are needed for the implementation, also consider creating even sub issues. (for example show a comment, delete a comment, update a comment, etc.). This helps you and also everyone else following the implementation
+- [ ] **Keep it small** To make it easier for you and also all reviewers try to keep merge requests small and merge into a feature branch if needed. To accomplish that you need to plan that from the start. Different methods are:
+ - [ ] **Skeleton based plan** Start with an MR that has the skeleton of the components with placeholder content. In following MRs you can fill the components with interactivity. This also makes it easier to spread out development on multiple people.
+ - [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features
+ - [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request')
+- [ ] **Setup** Is there any specific setup needed for your implementation (for example a kubernetes cluster)? Then let everyone know if it is not already mentioned where they can find documentation (if it doesn't exist - create it)
+- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+
+#### During development
+
+- [ ] Check off tasks on your created task list to keep everyone updated on the progress
+- [ ] [Share your work early with reviewers/maintainers](#share-your-work-early)
+- [ ] Share your work with UXer and Product Manager with Screenshots and/or [GIF's](https://about.gitlab.com/handbook/product/making-gifs/). They are easy to create for you and keep them up to date.
+- [ ] If you are blocked on something let everyone on the issue know through a comment.
+- [ ] Are you unable to work on this issue for a longer period of time, also let everyone know.
+- [ ] **Documentation** Update/add docs for the new feature, see `docs/`. Ping one of the documentation experts/reviewers
+
+#### Finishing development + Review
+
+- [ ] **Keep it in the scope** Try to focus on the actual scope and avoid a scope creep during review and keep new things to new issues.
+- [ ] **Performance** Have you checked performance? For example do the same thing with 500 comments instead of 1. Document the tests and possible findings in the MR so a reviewer can directly see it.
+- [ ] Have you tested with a variety of our [supported browsers](../../install/requirements.md#supported-web-browsers)? You can use [browserstack](https://www.browserstack.com/) to be able to access a wide variety of browsers and operating systems.
+- [ ] Did you check the mobile view?
+- [ ] Check the built webpack bundle (For the report run `WEBPACK_REPORT=true gdk run`, then open `webpack-report/index.html`) if we have unnecessary bloat due to wrong references, including libraries multiple times, etc.. If you need help contact the webpack [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+- [ ] **Tests** Not only greenfield tests - Test also all bad cases that come to your mind.
+- [ ] If you have multiple MR's then also smoke test against the final merge.
+- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
+- [ ] Smoke test of the RC on dev., staging., canary deployments and .com
+- [ ] Follow up on issues that came out of the review. Create issues for discovered edge cases that should be covered in future iterations.
+
+---
+
+### Share your work early
+1. Before writing code, ensure your vision of the architecture is aligned with
+GitLab's architecture.
+1. Add a diagram to the issue and ask a frontend architect in the slack channel `#fe_architectural` about it.
+
+ ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
+
+1. Don't take more than one week between starting work on a feature and
+sharing a Merge Request with a reviewer or a maintainer.
+
+### Vue features
+1. Follow the steps in [Vue.js Best Practices](vue.md)
+1. Follow the style guide.
+1. Only a handful of people are allowed to merge Vue related features.
+Reach out to one of Vue experts early in this process.
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index b288ee95722..3d8da6accc1 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -1,26 +1,44 @@
-# Icons
+# Icons and SVG Illustrations
-We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are only loaded once and then referenced through an ID. The sprite SVG is located under `/assets/icons.svg`. Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome usages.
+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`.
-### Usage in HAML/Rails
+## Icons
-To use a sprite Icon in HAML or Rails we use a specific helper function :
+We are using SVG Icons in GitLab with a SVG Sprite.
+This means the icons are only loaded once, and are referenced through an ID.
+The sprite SVG is located under `/assets/icons.svg`.
+
+Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome icons.
-`sprite_icon(icon_name, size: nil, css_class: '')`
+### Usage in HAML/Rails
-**icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
+To use a sprite Icon in HAML or Rails we use a specific helper function :
-**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
+```ruby
+sprite_icon(icon_name, size: nil, css_class: '')
+```
-**css_class (optional)** If you want to add additional css classes
+- **icon_name** Use the icon_name that you can find in the SVG Sprite
+ ([Overview is available here][svg-preview]).
+- **size (optional)** Use one of the following sizes : 16, 24, 32, 48, 72 (this will be translated into a `s16` class)
+- **css_class (optional)** If you want to add additional css classes
**Example**
-`= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
+```haml
+= sprite_icon('issues', size: 72, css_class: 'icon-danger')
+```
**Output from example above**
-`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
+```html
+<svg class="s72 icon-danger">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use>
+</svg>
+```
### Usage in Vue
@@ -28,33 +46,71 @@ We have a special Vue component for our sprite icons in `\vue_shared\components\
Sample usage :
-`<icon
- name="retry"
- :size="32"
- css-classes="top"
- />`
-
-**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
-
-**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes)
-
-**css-classes (optional)** Additional CSS Classes to add to the svg tag.
+```javascript
+<script>
+import Icon from "~/vue_shared/components/icon.vue"
+
+export default {
+ components: {
+ Icon,
+ },
+};
+<script>
+<template>
+ <icon
+ name="issues"
+ :size="72"
+ css-classes="icon-danger"
+ />
+</template>
+```
+
+- **name** Name of the Icon in the SVG Sprite ([Overview is available here][svg-preview]).
+- **size (optional)** Number value for the size which is then mapped to a specific CSS class
+ (Available Sizes: 8, 12, 16, 18, 24, 32, 48, 72 are mapped to `sXX` css classes)
+- **css-classes (optional)** Additional CSS Classes to add to the svg tag.
### Usage in HTML/JS
-Please use the following function inside JS to render an icon :
+Please use the following function inside JS to render an icon:
`gl.utils.spriteIcon(iconName)`
-## Adding a new icon to the sprite
+## SVG Illustrations
-All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
+Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers.
+Please use the class `svg-content` around it to ensure nice rendering.
-To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced.
+### Usage in HAML/Rails
-# SVG Illustrations
+**Example**
-Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers. Please use the class `svg-content` around it to ensure nice rendering. The illustrations are also organised in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository (as they are then automatically optimised).
+```haml
+.svg-content
+ = image_tag 'illustrations/merge_requests.svg'
+```
-**Example**
+### Usage in Vue
-`= image_tag 'illustrations/merge_requests.svg'`
+To use an SVG illustrations in a template provide the path as a property and display it through a standard img tag.
+
+Component:
+
+```js
+<script>
+export default {
+ props: {
+ svgIllustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+<script>
+<template>
+ <img :src="svgIllustrationPath" />
+</template>
+```
+
+[npm]: https://www.npmjs.com/package/@gitlab-org/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/index.md b/doc/development/fe_guide/index.md
index 2280cf79f86..11b9a2e6a64 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -1,15 +1,22 @@
# Frontend Development Guidelines
+> **Notice:**
+We are currently in the process of re-writing our development guide to make it easier to find information. The new guide is still WIP but viewable in [development/new_fe_guide](../new_fe_guide/index.md)
+
This document describes various guidelines to ensure consistency and quality
across GitLab's frontend team.
## Overview
-GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
-[Hamlit][hamlit]. Be wary of [the limitations that come with using
-Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
-modern ECMAScript standards supported through [Babel][babel] and ES module
-support through [webpack][webpack].
+GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] and also a JavaScript based Frontend with [Vue.js][vue].
+Be wary of [the limitations that come with using Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
+modern ECMAScript standards supported through [Babel][babel] and ES module support through [webpack][webpack].
+
+### Javascript development
+
+[Vue.js][vue] is used for particularly advanced, dynamic elements and based on previous iterations [jQuery][jquery] is used in lot of places through the application's JavaScript.
+
+We also use [Axios][axios] to handle all of our network requests.
We also utilize [webpack][webpack] to handle the bundling, minification, and
compression of our assets.
@@ -18,66 +25,37 @@ Working with our frontend assets requires Node (v6.0 or greater) and Yarn
(v1.2 or greater). You can find information on how to install these on our
[installation guide][install].
-[jQuery][jquery] is used throughout the application's JavaScript, with
-[Vue.js][vue] for particularly advanced, dynamic elements.
-
-We also use [Axios][axios] to handle all of our network requests.
-
### Browser Support
For our currently-supported browsers, see our [requirements][requirements].
---
-## Development Process
-
-### Share your work early
-1. Before writing code guarantee your vision of the architecture is aligned with
-GitLab's architecture.
-1. Add a diagram to the issue and ask a Frontend Architecture about it.
-
- ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
-
-1. Don't take more than one week between starting work on a feature and
-sharing a Merge Request with a reviewer or a maintainer.
-
-### Vue features
-1. Follow the steps in [Vue.js Best Practices](vue.md)
-1. Follow the style guide.
-1. Only a handful of people are allowed to merge Vue related features.
-Reach out to one of Vue experts early in this process.
-
-
----
+## [Development Process](development_process.md)
+How we plan and execute the work on the frontend.
## [Architecture](architecture.md)
How we go about making fundamental design decisions in GitLab's frontend team
or make changes to our frontend development guidelines.
----
-
## [Testing](../testing_guide/frontend_testing.md)
-
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
----
-
## [Design Patterns](design_patterns.md)
Common JavaScript design patterns in GitLab's codebase.
----
-
## [Vue.js Best Practices](vue.md)
Vue specific design patterns and practices.
----
+## [Vuex](vuex.md)
+Vuex specific design patterns and practices.
## [Axios](axios.md)
Axios specific practices and gotchas.
-## [Icons](icons.md)
-How we use SVG for our Icons.
+## [Icons and Illustrations](icons.md)
+How we use SVG for our Icons and Illustrations.
## [Components](components.md)
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 7b5fa6ca42f..284b4b53334 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -236,7 +236,7 @@ export class Foo {
}
```
-On the other hand, if a class only needs to extend a third party/add event listeners in some specific cases, they should be initialized oustside of the constructor.
+On the other hand, if a class only needs to extend a third party/add event listeners in some specific cases, they should be initialized outside of the constructor.
1. Prefer `.map`, `.reduce` or `.filter` over `.forEach`
A forEach will most likely cause side effects, it will be mutating the array being iterated. Prefer using `.map`,
@@ -310,7 +310,7 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
}));
```
-1. Don not use a singleton for the service or the store
+1. Do not use a singleton for the service or the store
```javascript
// bad
class Store {
@@ -328,9 +328,11 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
}
}
```
+1. Use `.vue` for Vue templates. Do not use `%template` in HAML.
#### Naming
-1. **Extensions**: Use `.vue` extension for Vue components.
+
+1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371]).
1. **Reference Naming**: Use PascalCase for their instances:
```javascript
// bad
@@ -364,6 +366,8 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
<component my-prop="prop" />
```
+[#34371]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34371
+
#### Alignment
1. Follow these alignment styles for the template method:
1. With more than one attribute, all attributes should be on a new line:
@@ -628,7 +632,7 @@ Useful links:
// good
<span title="tooltip text">Foo</span>
- $('span').tooltip('fixTitle');
+ $('span').tooltip('_fixTitle');
```
### The Javascript/Vue Accord
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 944b56a65f1..e31ee087358 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -1,36 +1,14 @@
# Vue
-For more complex frontend features, we recommend using Vue.js. It shares
-some ideas with React.js as well as Angular.
-
To get started with Vue, read through [their documentation][vue-docs].
-## When to use Vue.js
-
-We recommend using Vue for more complex features. Here are some guidelines for when to use Vue.js:
-
-- If you are starting a new feature or refactoring an old one that highly interacts with the DOM;
-- For real time data updates;
-- If you are creating a component that will be reused elsewhere;
-
-## When not to use Vue.js
-
-We don't want to refactor all GitLab frontend code into Vue.js, here are some guidelines for
-when not to use Vue.js:
-
-- Adding or changing static information;
-- Features that highly depend on jQuery will be hard to work with Vue.js;
-- Features without reactive data;
-
-As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions.
-
## Vue architecture
All new features built with Vue.js must follow a [Flux architecture][flux].
The main goal we are trying to achieve is to have only one data flow and only one data entry.
In order to achieve this goal, you can either use [vuex](#vuex) or use the [store pattern][state-management], explained below:
-Each Vue bundle needs a Store - where we keep all the data -,a Service - that we use to communicate with the server - and a main Vue component.
+Each Vue bundle needs a Store - where we keep all the data -, a Service - that we use to communicate with the server - and a main Vue component.
Think of the Main Vue Component as the entry point of your application. This is the only smart
component that should exist in each Vue feature.
@@ -39,7 +17,7 @@ This component is responsible for:
1. Calling the Store to store the data received
1. Mounting all the other components
- ![Vue Architecture](img/vue_arch.png)
+![Vue Architecture](img/vue_arch.png)
You can also read about this architecture in vue docs about [state management][state-management]
and about [one way data flow][one-way-data-flow].
@@ -57,15 +35,15 @@ new_feature
│ └── ...
├── stores
│ └── new_feature_store.js
-├── services
+├── services # only when not using vuex
│ └── new_feature_service.js
-├── new_feature_bundle.js
+├── index.js
```
_For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
-### A `*_bundle.js` file
+### A `index.js` file
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
@@ -73,14 +51,14 @@ of the new feature should be.
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
-Don't forget to follow [these steps.][page_specific_javascript]
+Don't forget to follow [these steps][page_specific_javascript].
### Bootstrapping Gotchas
-#### Providing data from Haml to JavaScript
+#### Providing data from HAML to JavaScript
While mounting a Vue application may be a need to provide data from Rails to JavaScript.
To do that, provide the data through `data` attributes in the HTML element and query them while mounting the application.
-_Note:_ You should only do this while initing the application, because the mounted element will be replaced with Vue-generated DOM.
+_Note:_ You should only do this while initializing the application, because the mounted element will be replaced with Vue-generated DOM.
The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function
instead of querying the DOM inside the main vue component is that makes tests easier by avoiding the need to
@@ -90,6 +68,7 @@ create a fixture or an HTML element in the unit test. See the following example:
// haml
.js-vue-app{ data: { endpoint: 'foo' }}
+// index.js
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
data() {
@@ -109,13 +88,11 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
```
#### Accessing the `gl` object
-When we need to query the `gl` object for data that won't change during the application's lyfecyle, we should do it in the same place where we query the DOM.
+When we need to query the `gl` object for data that won't change during the application's life cyle, we should do it in the same place where we query the DOM.
By following this practice, we can avoid the need to mock the `gl` object, which will make tests easier.
It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component:
-##### example:
```javascript
-
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
render(createElement) {
@@ -143,31 +120,12 @@ in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
-#### Components Gotchas
-1. Using SVGs in components: To use an SVG in a template we need to make it a property we can access through the component.
-A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them.
-The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies.
-
-```javascript
-// bad
-import svg from 'svg.svg';
-data() {
- return {
- myIcon: svg,
- };
-};
-
-// good
-import svg from 'svg.svg';
-computed: {
- myIcon() {
- return svg;
- }
-}
-```
-
### A folder for the Store
+#### Vuex
+Check this [page](vuex.md) for more details.
+
+#### Flux like state management
The Store is a class that allows us to manage the state in a single
source of truth. It is not aware of the service or the components.
@@ -176,6 +134,8 @@ itself, please read this guide: [State Management][state-management]
### A folder for the Service
+**If you are using Vuex you won't need this step**
+
The Service is a class used only to communicate with the server.
It does not store or manipulate any data. It is not aware of the store or the components.
We use [axios][axios] to communicate with the server.
@@ -183,13 +143,13 @@ Refer to [axios](axios.md) for more details.
Axios instance should only be imported in the service file.
- ```javascript
- import axios from 'javascripts/lib/utils/axios_utils';
- ```
+```javascript
+import axios from '~/lib/utils/axios_utils';
+```
### End Result
-The following example shows an application:
+The following example shows an application:
```javascript
// store.js
@@ -197,8 +157,8 @@ export default class Store {
/**
* This is where we will iniatialize the state of our data.
- * Usually in a small SPA you don't need any options when starting the store. In the case you do
- * need guarantee it's an Object and it's documented.
+ * Usually in a small SPA you don't need any options when starting the store.
+ * In that case you do need guarantee it's an Object and it's documented.
*
* @param {Object} options
*/
@@ -206,7 +166,7 @@ export default class Store {
this.options = options;
// Create a state object to handle all our data in the same place
- this.todos = []:
+ this.todos = [];
}
setTodos(todos = []) {
@@ -227,7 +187,7 @@ export default class Store {
}
// service.js
-import axios from 'javascripts/lib/utils/axios_utils'
+import axios from '~/lib/utils/axios_utils'
export default class Service {
constructor(options) {
@@ -253,8 +213,8 @@ export default {
type: Object,
required: true,
},
- }
-}
+ },
+};
</script>
<template>
<div>
@@ -273,6 +233,9 @@ import Store from 'store';
import Service from 'service';
import TodoComponent from 'todoComponent';
export default {
+ components: {
+ todo: TodoComponent,
+ },
/**
* Although most data belongs in the store, each component it's own state.
* We want to show a loading spinner while we are fetching the todos, this state belong
@@ -291,12 +254,8 @@ export default {
};
},
- components: {
- todo: TodoComponent,
- },
-
created() {
- this.service = new Service('todos');
+ this.service = new Service('/todos');
this.getTodos();
},
@@ -305,9 +264,9 @@ export default {
getTodos() {
this.isLoading = true;
- this.service.getTodos()
- .then(response => response.json())
- .then((response) => {
+ this.service
+ .getTodos()
+ .then(response => {
this.store.setTodos(response);
this.isLoading = false;
})
@@ -317,18 +276,21 @@ export default {
});
},
- addTodo(todo) {
- this.service.addTodo(todo)
- then(response => response.json())
- .then((response) => {
- this.store.addTodo(response);
- })
- .catch(() => {
- // Show an error
- });
- }
- }
-}
+ addTodo(event) {
+ this.service
+ .addTodo({
+ title: 'New entry',
+ text: `You clicked on ${event.target.tagName}`,
+ })
+ .then(response => {
+ this.store.addTodo(response);
+ })
+ .catch(() => {
+ // Show an error
+ });
+ },
+ },
+};
</script>
<template>
<div class="container">
@@ -354,7 +316,7 @@ export default {
<div>
</template>
-// bundle.js
+// index.js
import todoComponent from 'todos_main_component.vue';
new Vue({
@@ -386,76 +348,79 @@ Each Vue component has a unique output. This output is always present in the ren
Although we can test each method of a Vue component individually, our goal must be to test the output
of the render/template function, which represents the state at all times.
-Make use of Vue Resource Interceptors to mock data returned by the service.
+Make use of the [axios mock adapter](axios.md#mock-axios-response-on-tests) to mock data returned.
Here's how we would test the Todo App above:
```javascript
-import component from 'todos_main_component';
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
describe('Todos App', () => {
- it('should render the loading state while the request is being made', () => {
+ let vm;
+ let mock;
+
+ beforeEach(() => {
+ // Create a mock adapter for stubbing axios API requests
+ mock = new MockAdapter(axios);
+
const Component = Vue.extend(component);
- const vm = new Component().$mount();
+ // Mount the Component
+ vm = new Component().$mount();
+ });
+ afterEach(() => {
+ // Reset the mock adapter
+ mock.restore();
+ // Destroy the mounted component
+ vm.$destroy();
+ });
+
+ it('should render the loading state while the request is being made', () => {
expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
});
- describe('with data', () => {
- // Mock the service to return data
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([{
+ it('should render todos returned by the endpoint', done => {
+ // Mock the get request on the API endpoint to return data
+ mock.onGet('/todos').replyOnce(200, [
+ {
title: 'This is a todo',
- body: 'This is the text'
- }]), {
- status: 200,
- }));
- };
-
- let vm;
-
- beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
-
- const Component = Vue.extend(component);
+ text: 'This is the text',
+ },
+ ]);
- vm = new Component().$mount();
+ Vue.nextTick(() => {
+ const items = vm.$el.querySelectorAll('.js-todo-list div')
+ expect(items.length).toBe(1);
+ expect(items[0].textContent).toContain('This is the text');
+ done();
});
+ });
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
- });
+ it('should add a todos on button click', (done) => {
+ // Mock the put request and check that the sent data object is correct
+ mock.onPut('/todos').replyOnce((req) => {
+ expect(req.data).toContain('text');
+ expect(req.data).toContain('title');
- it('should render todos', (done) => {
- setTimeout(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1);
- done();
- }, 0);
+ return [201, {}];
});
- });
- describe('add todo', () => {
- let vm;
- beforeEach(() => {
- const Component = Vue.extend(component);
- vm = new Component().$mount();
- });
- it('should add a todos', (done) => {
- setTimeout(() => {
- vm.$el.querySelector('.js-add-todo').click();
+ vm.$el.querySelector('.js-add-todo').click();
- // Add a new interceptor to mock the add Todo request
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
- });
- }, 0);
+ // Add a new interceptor to mock the add Todo request
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
+ done();
});
});
});
```
-#### `mountComponent` helper
+
+### `mountComponent` helper
There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
```javascript
@@ -468,208 +433,10 @@ const data = {prop: 'foo'};
const vm = mountComponent(Component, data);
```
-#### Test the component's output
+### Test the component's output
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:
-### Stubbing API responses
-Refer to [mock axios](axios.md#mock-axios-response-on-tests)
-
-
-## Vuex
-To manage the state of an application you may use [Vuex][vuex-docs].
-
-_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
-
-### Separation of concerns
-Vuex is composed of State, Getters, Mutations, Actions and Modules.
-
-When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state.
-_Note:_ The action itself will not update the state, only a mutation should update the state.
-
-#### File structure
-When using Vuex at GitLab, separate this concerns into different files to improve readability. If you can, separate the Mutation Types as well:
-
-```
-└── store
- ├── index.js # where we assemble modules and export the store
- ├── actions.js # actions
- ├── mutations.js # mutations
- ├── getters.js # getters
- └── mutation_types.js # mutation types
-```
-The following examples show an application that lists and adds users to the state.
-
-##### `index.js`
-This is the entry point for our store. You can use the following as a guide:
-
-```javascript
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-
-Vue.use(Vuex);
-
-export default new Vuex.Store({
- actions,
- getters,
- mutations,
- state: {
- users: [],
- },
-});
-```
-_Note:_ If the state of the application is too complex, an individual file for the state may be better.
-
-##### `actions.js`
-An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
-
-```javascript
- import * as types from './mutation_types';
-
- export const addUser = ({ commit }, user) => {
- commit(types.ADD_USER, user);
- };
-```
-
-To dispatch an action from a component, use the `mapActions` helper:
-```javascript
-import { mapActions } from 'vuex';
-
-{
- methods: {
- ...mapActions([
- 'addUser',
- ]),
- onClickUser(user) {
- this.addUser(user);
- },
- },
-};
-```
-
-##### `getters.js`
-Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`:
-
-```javascript
-// get all the users with pets
-export getUsersWithPets = (state, getters) => {
- return state.users.filter(user => user.pet !== undefined);
-};
-```
-
-To access a getter from a component, use the `mapGetters` helper:
-```javascript
-import { mapGetters } from 'vuex';
-
-{
- computed: {
- ...mapGetters([
- 'getUsersWithPets',
- ]),
- },
-};
-```
-
-##### `mutations.js`
-The only way to actually change state in a Vuex store is by committing a mutation.
-
-```javascript
- import * as types from './mutation_types';
-
- export default {
- [types.ADD_USER](state, user) {
- state.users.push(user);
- },
- };
-```
-
-##### `mutations_types.js`
-From [vuex mutations docs][vuex-mutations]:
-> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
-
-```javascript
-export const ADD_USER = 'ADD_USER';
-```
-
-### How to include the store in your application
-The store should be included in the main component of your application:
-```javascript
- // app.vue
- import store from 'store'; // it will include the index.js file
-
- export default {
- name: 'application',
- store,
- ...
- };
-```
-
-### Vuex Gotchas
-1. Avoid calling a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs:
-
- > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
-
- ```javascript
- // component.vue
-
- // bad
- created() {
- this.$store.commit('mutation');
- }
-
- // good
- created() {
- this.$store.dispatch('action');
- }
- ```
-1. When possible, use mutation types instead of hardcoding strings. It will be less error prone.
-1. The State will be accessible in all components descending from the use where the store is instantiated.
-
-### Testing Vuex
-#### Testing Vuex concerns
-Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations.
-
-#### Testing components that need a store
-Smaller components might use `store` properties to access the data.
-In order to write unit tests for those components, we need to include the store and provide the correct state:
-
-```javascript
-//component_spec.js
-import Vue from 'vue';
-import store from './store';
-import component from './component.vue'
-
-describe('component', () => {
- let vm;
- let Component;
-
- beforeEach(() => {
- Component = Vue.extend(issueActions);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should show a user', () => {
- const user = {
- name: 'Foo',
- age: '30',
- };
-
- // populate the store
- store.dipatch('addUser', user);
-
- vm = new Component({
- store,
- propsData: props,
- }).$mount();
- });
-});
-```
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
@@ -681,9 +448,4 @@ describe('component', () => {
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[flux]: https://facebook.github.io/flux
-[vuex-docs]: https://vuex.vuejs.org
-[vuex-structure]: https://vuex.vuejs.org/en/structure.html
-[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
-[vuex-testing]: https://vuex.vuejs.org/en/testing.html
[axios]: https://github.com/axios/axios
-[axios-interceptors]: https://github.com/axios/axios#interceptors
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
new file mode 100644
index 00000000000..858b03c60bf
--- /dev/null
+++ b/doc/development/fe_guide/vuex.md
@@ -0,0 +1,374 @@
+# Vuex
+To manage the state of an application you should use [Vuex][vuex-docs].
+
+_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
+
+## Separation of concerns
+Vuex is composed of State, Getters, Mutations, Actions and Modules.
+
+When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state.
+_Note:_ The action itself will not update the state, only a mutation should update the state.
+
+## File structure
+When using Vuex at GitLab, separate this concerns into different files to improve readability:
+
+```
+└── store
+ ├── index.js # where we assemble modules and export the store
+ ├── actions.js # actions
+ ├── mutations.js # mutations
+ ├── getters.js # getters
+ ├── state.js # state
+ └── mutation_types.js # mutation types
+```
+The following example shows an application that lists and adds users to the state.
+(For a more complex example implementation take a look at the security applications store in [here](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports/store))
+
+### `index.js`
+This is the entry point for our store. You can use the following as a guide:
+
+```javascript
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () => new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state,
+});
+export default createStore();
+```
+
+### `state.js`
+The first thing you should do before writing any code is to design the state.
+
+Often we need to provide data from haml to our Vue application. Let's store it in the state for better access.
+
+```javascript
+ export default {
+ endpoint: null,
+
+ isLoading: false,
+ error: null,
+
+ isAddingUser: false,
+ errorAddingUser: false,
+
+ users: [],
+ };
+```
+
+#### Access `state` properties
+You can use `mapState` to access state properties in the components.
+
+### `actions.js`
+An action is a payload of information to send data from our application to our store.
+
+An action is usually composed by a `type` and a `payload` and they describe what happened.
+Enforcing that every change is described as an action lets us have a clear understanding of what is going on in the app.
+
+In this file, we will write the actions that will call the respective mutations:
+
+```javascript
+ import * as types from './mutation_types';
+ import axios from '~/lib/utils/axios-utils';
+ import createFlash from '~/flash';
+
+ export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
+ export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data);
+ export const receiveUsersError = ({ commit }, error) => commit(types.REQUEST_USERS_ERROR, error);
+
+ export const fetchUsers = ({ state, dispatch }) => {
+ dispatch('requestUsers');
+
+ axios.get(state.endpoint)
+ .then(({ data }) => dispatch('receiveUsersSuccess', data))
+ .catch((error) => {
+ dispatch('receiveUsersError', error)
+ createFlash('There was an error')
+ });
+ }
+
+ export const requestAddUser = ({ commit }) => commit(types.REQUEST_ADD_USER);
+ export const receiveAddUserSuccess = ({ commit }, data) => commit(types.RECEIVE_ADD_USER_SUCCESS, data);
+ export const receiveAddUserError = ({ commit }, error) => commit(types.REQUEST_ADD_USER_ERROR, error);
+
+ export const addUser = ({ state, dispatch }, user) => {
+ dispatch('requestAddUser');
+
+ axios.post(state.endpoint, user)
+ .then(({ data }) => dispatch('receiveAddUserSuccess', data))
+ .catch((error) => dispatch('receiveAddUserError', error));
+ }
+```
+
+#### Actions Pattern: `request` and `receive` namespaces
+When a request is made we often want to show a loading state to the user.
+
+Instead of creating an action to toggle the loading state and dispatch it in the component,
+create:
+1. An action `requestSomething`, to toggle the loading state
+1. An action `receiveSomethingSuccess`, to handle the success callback
+1. An action `receiveSomethingError`, to handle the error callback
+1. An action `fetchSomething` to make the request.
+ 1. In case your application does more than a `GET` request you can use these as examples:
+ 1. `PUT`: `createSomething`
+ 2. `POST`: `updateSomething`
+ 3. `DELETE`: `deleteSomething`
+
+The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component
+The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
+
+By following this pattern we guarantee:
+1. All applications follow the same pattern, making it easier for anyone to maintain the code
+1. All data in the application follows the same lifecycle pattern
+1. Actions are contained and human friendly
+1. Unit tests are easier
+1. Actions are simple and straightforward
+
+#### Dispatching actions
+To dispatch an action from a component, use the `mapActions` helper:
+```javascript
+import { mapActions } from 'vuex';
+
+{
+ methods: {
+ ...mapActions([
+ 'addUser',
+ ]),
+ onClickUser(user) {
+ this.addUser(user);
+ },
+ },
+};
+```
+
+### `mutations.js`
+The mutations specify how the application state changes in response to actions sent to the store.
+The only way to change state in a Vuex store should be by committing a mutation.
+
+**It's a good idea to think of the state before writing any code.**
+
+Remember that actions only describe that something happened, they don't describe how the application state changes.
+
+**Never commit a mutation directly from a component**
+
+```javascript
+ import * as types from './mutation_types';
+
+ export default {
+ [types.REQUEST_USERS](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_USERS_SUCCESS](state, data) {
+ // Do any needed data transformation to the received payload here
+ state.users = data;
+ state.isLoading = false;
+ },
+ [types.REQUEST_USERS_ERROR](state, error) {
+ state.isLoading = false;
+ },
+ [types.REQUEST_ADD_USER](state, user) {
+ state.isAddingUser = true;
+ },
+ [types.RECEIVE_ADD_USER_SUCCESS](state, user) {
+ state.isAddingUser = false;
+ state.users.push(user);
+ },
+ [types.REQUEST_ADD_USER_ERROR](state, error) {
+ state.isAddingUser = true;
+ state.errorAddingUser = error;
+ },
+ };
+```
+
+### `getters.js`
+Sometimes we may need to get derived state based on store state, like filtering for a specific prop.
+Using a getter will also cache the result based on dependencies due to [how computed props work](https://vuejs.org/v2/guide/computed.html#Computed-Caching-vs-Methods)
+This can be done through the `getters`:
+
+```javascript
+// get all the users with pets
+export const getUsersWithPets = (state, getters) => {
+ return state.users.filter(user => user.pet !== undefined);
+};
+```
+
+To access a getter from a component, use the `mapGetters` helper:
+```javascript
+import { mapGetters } from 'vuex';
+
+{
+ computed: {
+ ...mapGetters([
+ 'getUsersWithPets',
+ ]),
+ },
+};
+```
+
+### `mutations_types.js`
+From [vuex mutations docs][vuex-mutations]:
+> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
+
+```javascript
+export const ADD_USER = 'ADD_USER';
+```
+
+### How to include the store in your application
+The store should be included in the main component of your application:
+```javascript
+ // app.vue
+ import store from 'store'; // it will include the index.js file
+
+ export default {
+ name: 'application',
+ store,
+ ...
+ };
+```
+
+### Communicating with the Store
+```javascript
+<script>
+import { mapActions, mapState, mapGetters } from 'vuex';
+import store from './store';
+
+export default {
+ store,
+ computed: {
+ ...mapGetters([
+ 'getUsersWithPets'
+ ]),
+ ...mapState([
+ 'isLoading',
+ 'users',
+ 'error',
+ ]),
+ },
+ methods: {
+ ...mapActions([
+ 'fetchUsers',
+ 'addUser',
+ ]),
+
+ onClickAddUser(data) {
+ this.addUser(data);
+ }
+ },
+
+ created() {
+ this.fetchUsers()
+ }
+}
+</script>
+<template>
+ <ul>
+ <li v-if="isLoading">
+ Loading...
+ </li>
+ <li v-else-if="error">
+ {{ error }}
+ </li>
+ <template v-else>
+ <li
+ v-for="user in users"
+ :key="user.id"
+ >
+ {{ user }}
+ </li>
+ </template>
+ </ul>
+</template>
+```
+
+### Vuex Gotchas
+1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency throughout the application. From Vuex docs:
+
+ > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
+
+ ```javascript
+ // component.vue
+
+ // bad
+ created() {
+ this.$store.commit('mutation');
+ }
+
+ // good
+ created() {
+ this.$store.dispatch('action');
+ }
+ ```
+1. Use mutation types instead of hardcoding strings. It will be less error prone.
+1. The State will be accessible in all components descending from the use where the store is instantiated.
+
+### Testing Vuex
+#### Testing Vuex concerns
+Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations.
+
+#### Testing components that need a store
+Smaller components might use `store` properties to access the data.
+In order to write unit tests for those components, we need to include the store and provide the correct state:
+
+```javascript
+//component_spec.js
+import Vue from 'vue';
+import { createStore } from './store';
+import component from './component.vue'
+
+describe('component', () => {
+ let store;
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(issueActions);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should show a user', () => {
+ const user = {
+ name: 'Foo',
+ age: '30',
+ };
+
+ store = createStore();
+
+ // populate the store
+ store.dispatch('addUser', user);
+
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+});
+```
+
+#### Testing Vuex actions and getters
+Because we're currently using [`babel-plugin-rewire`](https://github.com/speedskater/babel-plugin-rewire), you may encounter the following error when testing your Vuex actions and getters:
+`[vuex] actions should be function or object with "handler" function`
+
+To prevent this error from happening, you need to export an empty function as `default`:
+```
+// getters.js or actions.js
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
+```
+
+[vuex-docs]: https://vuex.vuejs.org
+[vuex-structure]: https://vuex.vuejs.org/en/structure.html
+[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
+[vuex-testing]: https://vuex.vuejs.org/en/testing.html
diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md
index 34a02bd2c3c..fdbd7f1fa37 100644
--- a/doc/development/file_storage.md
+++ b/doc/development/file_storage.md
@@ -84,7 +84,7 @@ The `RecordsUploads::Concern` concern will create an `Upload` entry for every fi
By including the `ObjectStorage::Concern` in the `GitlabUploader` derived class, you may enable the object storage for this uploader. To enable the object storage
in your uploader, you need to either 1) include `RecordsUpload::Concern` and prepend `ObjectStorage::Extension::RecordsUploads` or 2) mount the uploader and create a new field named `<mount>_store`.
-The `CarrierWave::Uploader#store_dir` is overriden to
+The `CarrierWave::Uploader#store_dir` is overridden to
- `GitlabUploader.base_dir` + `GitlabUploader.dynamic_segment` when the store is LOCAL
- `GitlabUploader.dynamic_segment` when the store is REMOTE (the bucket name is used to namespace)
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index b1bec84a2f3..0edcb23c7c5 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -270,7 +270,7 @@ If there are merge conflicts in the `gitlab.pot` file, you can delete the file
and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
The command also updates the translation files for each language: `locale/*/gitlab.po`
-These changes can be discarded, the languange files will be updated by Crowdin
+These changes can be discarded, the language files will be updated by Crowdin
automatically.
Discard all of them at once like this:
diff --git a/doc/development/img/state-model-issue.png b/doc/development/img/state-model-issue.png
deleted file mode 100644
index ee33b6886c6..00000000000
--- a/doc/development/img/state-model-issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/img/state-model-legend.png b/doc/development/img/state-model-legend.png
deleted file mode 100644
index 1c121f2588c..00000000000
--- a/doc/development/img/state-model-legend.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/img/state-model-merge-request.png b/doc/development/img/state-model-merge-request.png
deleted file mode 100644
index e00da10cac2..00000000000
--- a/doc/development/img/state-model-merge-request.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 2b4126b43ef..12badbe39b2 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -162,7 +162,7 @@ need for running complex operations to fetch the data. You should use Redis if
data should be cached for a certain time period instead of the duration of the
transaction.
-For example, say you process multiple snippets of text containiner username
+For example, say you process multiple snippets of text containing username
mentions (e.g. `Hello @alice` and `How are you doing @alice?`). By caching the
user objects for every username we can remove the need for running the same
query for every mention of `@alice`.
diff --git a/doc/development/new_fe_guide/development/accessibility.md b/doc/development/new_fe_guide/development/accessibility.md
index ed35f08432f..2a3a126ca5c 100644
--- a/doc/development/new_fe_guide/development/accessibility.md
+++ b/doc/development/new_fe_guide/development/accessibility.md
@@ -1,3 +1,48 @@
-# Accessibility
+# Accessiblity
+Using semantic HTML plays a key role when it comes to accessibility.
-> TODO: Add content
+## Accessible Rich Internet Applications - ARIA
+WAI-ARIA, the Accessible Rich Internet Applications specification, defines a way to make Web content and Web applications more accessible to people with disabilities.
+
+> Note: It is [recommended][using-aria] to use semantic elements as the primary method to achieve accessibility rather than adding aria attributes. Adding aria attributes should be seen as a secondary method for creating accessible elements.
+
+### Role
+The `role` attribute describes the role the element plays in the context of the document.
+
+Check the list of WAI-ARIA roles [here][roles]
+
+## Icons
+When using icons or images that aren't absolutely needed to understand the context, we should use `aria-hidden="true"`.
+
+On the other hand, if an icon is crucial to understand the context we should do one of the following:
+1. Use `aria-label` in the element with a meaningful description
+1. Use `aria-labelledby` to point to an element that contains the explanation for that icon
+
+## Form inputs
+In forms we should use the `for` attribute in the label statement:
+```
+<div>
+ <label for="name">Fill in your name:</label>
+ <input type="text" id="name" name="name">
+</div>
+```
+
+## Testing
+
+1. On MacOS you can use [VoiceOver][voice-over] by pressing `cmd+F5`.
+1. On Windows you can use [Narrator][narrator] by pressing Windows logo key + Ctrl + Enter.
+
+## Online resources
+
+- [Chrome Accessibility Developer Tools][dev-tools] for testing accessibility
+- [Audit Rules Page][audit-rules] for best practices
+- [Lighthouse Accessibility Score][lighthouse] for accessibility audits
+
+[using-aria]: https://www.w3.org/TR/using-aria/#notes2
+[dev-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[aria-w3c]: https://www.w3.org/TR/wai-aria-1.1/
+[roles]: https://www.w3.org/TR/wai-aria-1.1/#landmark_roles
+[voice-over]: https://www.apple.com/accessibility/mac/vision/
+[narrator]: https://www.microsoft.com/en-us/accessibility/windows
+[lighthouse]: https://developers.google.com/web/tools/lighthouse/scoring#a11y
diff --git a/doc/development/object_state_models.md b/doc/development/object_state_models.md
deleted file mode 100644
index 623bbf143ef..00000000000
--- a/doc/development/object_state_models.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Object state models
-
-## Diagrams
-
-[GitLab object state models](https://drive.google.com/drive/u/3/folders/0B5tDlHAM4iZINmpvYlJXcDVqMGc)
-
----
-
-## Legend
-
-![legend](img/state-model-legend.png)
-
----
-
-## Issue
-
-[`app/models/issue.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/issue.rb)
-![issue](img/state-model-issue.png)
-
----
-
-## Merge request
-
-[`app/models/merge_request.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/models/merge_request.rb)
-![merge request](img/state-model-merge-request.png) \ No newline at end of file
diff --git a/doc/development/ordering_table_columns.md b/doc/development/ordering_table_columns.md
index 249e70c7b0e..5d00e1f7a0c 100644
--- a/doc/development/ordering_table_columns.md
+++ b/doc/development/ordering_table_columns.md
@@ -30,7 +30,7 @@ example) at the end.
## Type Sizes
-While the PostgreSQL docuemntation
+While the PostgreSQL documentation
(https://www.postgresql.org/docs/current/static/datatype.html) contains plenty
of information we will list the sizes of common types here so it's easier to
look them up. Here "word" refers to the word size, which is 4 bytes for a 32
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index 12e90101139..26d3355e94d 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -2,7 +2,7 @@
QueryRecorder is a tool for detecting the [N+1 queries problem](http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) from tests.
-> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
+> Implemented in [spec/support/query_recorder.rb](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/support/helpers/query_recorder.rb) via [9c623e3e](https://gitlab.com/gitlab-org/gitlab-ce/commit/9c623e3e5d7434f2e30f7c389d13e5af4ede770a)
As a rule, merge requests [should not increase query counts](merge_request_performance_guidelines.md#query-counts). If you find yourself adding something like `.includes(:author, :assignee)` to avoid having `N+1` queries, consider using QueryRecorder to enforce this with a test. Without this, a new feature which causes an additional model to be accessed will silently reintroduce the problem.
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index fdfa1f10402..31addcaf675 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -65,12 +65,11 @@ To make sure that indices still fit. You could find great details in:
## Run tests
In order to run the test you can use the following commands:
-- `rake spinach` to run the spinach suite
- `rake spec` to run the rspec suite
- `rake karma` to run the karma test suite
- `rake gitlab:test` to run all the tests
-Note: Both `rake spinach` and `rake spec` takes significant time to pass.
+Note: `rake spec` takes significant time to pass.
Instead of running full test suite locally you can save a lot of time by running
a single test or directory related to your changes. After you submit merge request
CI will run full test suite for you. Green CI status in the merge request means
@@ -82,12 +81,10 @@ files it can find, also the ones in `/tmp`
To run a single test file you can use:
- `bin/rspec spec/controllers/commit_controller_spec.rb` for a rspec test
-- `bin/spinach features/project/issues/milestones.feature` for a spinach test
To run several tests inside one directory:
- `bin/rspec spec/requests/api/` for the rspec tests if you want to test API only
-- `bin/spinach features/profile/` for the spinach tests if you want to test only profile pages
### Speed-up tests, rake tasks, and migrations
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index df80cd9f584..a76a5096b69 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -12,8 +12,7 @@ Here are some things to keep in mind regarding test performance:
- `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
-- Don't mark a feature as requiring JavaScript (through `@javascript` in
- Spinach or `:js` in RSpec) unless it's _actually_ required for the test
+- Don't mark a feature as requiring JavaScript (through `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
[parallelization]: ci.md#test-suite-parallelization-on-the-ci
@@ -63,6 +62,8 @@ writing one](testing_levels.md#consider-not-writing-a-system-test)!
Sometimes you may need to debug Capybara tests by observing browser behavior.
+#### Live debug
+
You can pause Capybara and view the website on the browser by using the
`live_debug` method in your spec. The current page will be automatically opened
in your default browser.
@@ -90,6 +91,67 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load)
Note: `live_debug` only works on javascript enabled specs.
+#### Run `:js` spec in a visible browser
+
+Run the spec with `CHROME_HEADLESS=0`, e.g.:
+
+```
+CHROME_HEADLESS=0 bundle exec rspec some_spec.rb
+```
+
+The test will go by quickly, but this will give you an idea of what's happening.
+
+You can also add `byebug` or `binding.pry` to pause execution and step through
+the test.
+
+#### Screenshots
+
+We use the `capybara-screenshot` gem to automatically take a screenshot on
+failure. In CI you can download these files as job artifacts.
+
+Also, you can manually take screenshots at any point in a test by adding the
+methods below. Be sure to remove them when they are no longer needed! See
+https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots for
+more.
+
+Add `screenshot_and_save_page` in a `:js` spec to screenshot what Capybara
+"sees", and save the page source.
+
+Add `screenshot_and_open_image` in a `:js` spec to screenshot what Capybara
+"sees", and automatically open the image.
+
+### Fast unit tests
+
+Some classes are well-isolated from Rails and you should be able to test them
+without the overhead added by the Rails environment and Bundler's `:default`
+group's gem loading. In these cases, you can `require 'fast_spec_helper'`
+instead of `require 'spec_helper'` in your test file, and your test should run
+really fast since:
+
+- Gems loading is skipped
+- Rails app boot is skipped
+- gitlab-shell and Gitaly setup are skipped
+- Test repositories setup are skipped
+
+`fast_spec_helper` also support autoloading classes that are located inside the
+`lib/` directory. It means that as long as your class / module is using only
+code from the `lib/` directory you will not need to explicitly load any
+dependencies. `fast_spec_helper` also loads all ActiveSupport extensions,
+including core extensions that are commonly used in the Rails environment.
+
+Note that in some cases, you might still have to load some dependencies using
+`require_dependency` when a code is using gems or a dependency is not located
+in `lib/`.
+
+For example, if you want to test your code that is calling the
+`Gitlab::UntrustedRegexp` class, which under the hood uses `re2` library, you
+should either add `require_dependency 're2'` to files in your library that
+need `re2` gem, to make this requirement explicit, or you can add it to the
+spec itself, but the former is preferred.
+
+It takes around one second to load tests that are using `fast_spec_helper`
+instead of 30+ seconds in case of a regular `spec_helper`.
+
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
@@ -180,6 +242,11 @@ describe "#==" do
end
```
+### Prometheus tests
+
+Prometheus metrics may be preserved from one test run to another. To ensure that metrics are
+reset before each example, add the `:prometheus` tag to the Rspec test.
+
### Matchers
Custom matchers should be created to clarify the intent and/or hide the
@@ -281,14 +348,13 @@ All fixtures should be be placed under `spec/fixtures/`.
RSpec config files are files that change the RSpec config (i.e.
`RSpec.configure do |config|` blocks). They should be placed under
-`spec/support/config/`.
+`spec/support/`.
Each file should be related to a specific domain, e.g.
-`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
+`spec/support/capybara.rb`, `spec/support/carrierwave.rb`, etc.
-Helpers can be included in the `spec/support/config/rspec.rb` file. If a
-helpers module applies only to a certain kind of specs, it should add modifiers
-to the `config.include` call. For instance if
+If a helpers module applies only to a certain kind of specs, it should add
+modifiers to the `config.include` call. For instance if
`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
`type: :model` specs only, you would write the following:
@@ -299,6 +365,14 @@ RSpec.configure do |config|
end
```
+If a config file only consists of `config.include`, you can add these
+`config.include` directly in `spec/spec_helper.rb`.
+
+For very generic helpers, consider including them in the `spec/support/rspec.rb`
+file which is used by the `spec/fast_spec_helper.rb` file. See
+[Fast unit tests](#fast-unit-tests) for more details about the
+`spec/fast_spec_helper.rb` file.
+
---
[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md
index e90de55068d..0d8e150e090 100644
--- a/doc/development/testing_guide/ci.md
+++ b/doc/development/testing_guide/ci.md
@@ -24,8 +24,7 @@ Our current CI parallelization setup is as follows:
uploaded to S3.
After that, the next pipeline will use the up-to-date
-`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
-is used for Spinach tests as well.
+`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file.
### Monitoring
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md
index d10a797a142..21ec926414d 100644
--- a/doc/development/testing_guide/end_to_end_tests.md
+++ b/doc/development/testing_guide/end_to_end_tests.md
@@ -67,9 +67,9 @@ and examples in [the `qa/` directory][instance-qa-examples].
## Where can I ask for help?
-You can ask question in the `#qa` 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.
+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.
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 0a6f402d5d2..3b2b9c8c947 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -62,6 +62,7 @@ describe('.methodName', () => {
});
});
```
+
#### Testing promises
When testing Promises you should always make sure that the test is asynchronous and rejections are handled.
@@ -69,9 +70,9 @@ Your Promise chain should therefore end with a call of the `done` callback and `
```javascript
// Good
-it('tests a promise', (done) => {
+it('tests a promise', done => {
promise
- .then((data) => {
+ .then(data => {
expect(data).toBe(asExpected);
})
.then(done)
@@ -79,10 +80,10 @@ it('tests a promise', (done) => {
});
// Good
-it('tests a promise rejection', (done) => {
+it('tests a promise rejection', done => {
promise
.then(done.fail)
- .catch((error) => {
+ .catch(error => {
expect(error).toBe(expectedError);
})
.then(done)
@@ -91,48 +92,85 @@ it('tests a promise rejection', (done) => {
// Bad (missing done callback)
it('tests a promise', () => {
- promise
- .then((data) => {
- expect(data).toBe(asExpected);
- })
+ promise.then(data => {
+ expect(data).toBe(asExpected);
+ });
});
// Bad (missing catch)
-it('tests a promise', (done) => {
+it('tests a promise', done => {
promise
- .then((data) => {
+ .then(data => {
expect(data).toBe(asExpected);
})
- .then(done)
+ .then(done);
});
// Bad (use done.fail in asynchronous tests)
-it('tests a promise', (done) => {
+it('tests a promise', done => {
promise
- .then((data) => {
+ .then(data => {
expect(data).toBe(asExpected);
})
.then(done)
- .catch(fail)
+ .catch(fail);
});
// Bad (missing catch)
-it('tests a promise rejection', (done) => {
+it('tests a promise rejection', done => {
promise
- .catch((error) => {
+ .catch(error => {
expect(error).toBe(expectedError);
})
- .then(done)
+ .then(done);
});
```
-#### Stubbing
+#### Stubbing and Mocking
-For unit tests, you should stub methods that are unrelated to the current unit you are testing.
-If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely.
+Jasmine provides useful helpers `spyOn`, `spyOnProperty`, `jasmine.createSpy`,
+and `jasmine.createSpyObject` to facilitate replacing methods with dummy
+placeholders, and recalling when they are called and the arguments that are
+passed to them. These tools should be used liberally, to test for expected
+behavior, to mock responses, and to block unwanted side effects (such as a
+method that would generate a network request or alter `window.location`). The
+documentation for these methods can be found in the [jasmine introduction page](https://jasmine.github.io/2.0/introduction.html#section-Spies).
-For integration tests, you should stub methods that will effect the stability of the test if they
-execute their original behaviour. i.e. Network requests.
+Sometimes you may need to spy on a method that is directly imported by another
+module. GitLab has a custom `spyOnDependency` method which utilizes
+[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
+achieve this. It can be used like so:
+
+```javascript
+// my_module.js
+import { visitUrl } from '~/lib/utils/url_utility';
+
+export default function doSomething() {
+ visitUrl('/foo/bar');
+}
+
+// my_module_spec.js
+import doSomething from '~/my_module';
+
+describe('my_module', () => {
+ it('does something', () => {
+ const visitUrl = spyOnDependency(doSomething, 'visitUrl');
+
+ doSomething();
+ expect(visitUrl).toHaveBeenCalledWith('/foo/bar');
+ });
+});
+```
+
+Unlike `spyOn`, `spyOnDependency` expects its first parameter to be the default
+export of a module who's import you want to stub, rather than an object which
+contains a method you wish to stub (if the module does not have a default
+export, one is be generated by the babel plugin). The second parameter is the
+name of the import you wish to change. The result of the function is a Spy
+object which can be treated like any other jasmine spy object.
+
+Further documentation on the babel rewire pluign API can be found on
+[its repository Readme doc](https://github.com/speedskater/babel-plugin-rewire#babel-plugin-rewire).
### Vue.js unit tests
@@ -143,8 +181,8 @@ See this [section][vue-test].
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
-- `rake karma:fixtures` (re-)generates fixtures
-- `rake karma:tests` actually executes the tests
+* `rake karma:fixtures` (re-)generates fixtures
+* `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
is sufficient (and saves you some time).
@@ -179,6 +217,14 @@ yarn karma-start --filter-spec profile/account/components/
yarn karma-start -f vue_shared -f vue_mr_widget
```
+You can also use glob syntax to match files. Remember to put quotes around the
+glob otherwise your shell may split it into multiple arguments:
+
+```bash
+# Run all specs named `file_spec` within the IDE subdirectory
+yarn karma -f 'spec/javascripts/ide/**/file_spec.js'
+```
+
## RSpec feature integration tests
Information on setting up and running RSpec integration tests with
@@ -193,14 +239,14 @@ supported by the PhantomJS test runner which is used for both Karma and RSpec
tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
-- Array.from
-- Array.first
-- Async functions
-- Generators
-- Array destructuring
-- For..Of
-- Symbol/Symbol.iterator
-- Spread
+* Array.from
+* Array.first
+* Async functions
+* Generators
+* Array destructuring
+* For..Of
+* Symbol/Symbol.iterator
+* Spread
Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
@@ -234,34 +280,14 @@ describe "Admin::AbuseReports", :js do
end
```
-### Spinach errors due to missing JavaScript
-
-NOTE: **Note:** Since we are discouraging the use of Spinach when writing new
-feature tests, you shouldn't ever need to use this. This information is kept
-available for legacy purposes only.
-
-In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
-file for the failing spec, add the `@javascript` flag above the Scenario:
-
-```
-@javascript
-Scenario: Developer can approve merge request
- Given I am a "Shop" developer
- And I visit project "Shop" merge requests page
- And merge request 'Bug NS-04' must be approved
- And I click link "Bug NS-04"
- When I click link "Approve"
- Then I should see approved merge request "Bug NS-04"
-```
-
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
-[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
-[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
-[Capybara]: https://github.com/teamcapybara/capybara
-[Karma]: http://karma-runner.github.io/
-[Jasmine]: https://jasmine.github.io/
+[vue-test]: https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
+[rspec]: https://github.com/rspec/rspec-rails#feature-specs
+[capybara]: https://github.com/teamcapybara/capybara
+[karma]: http://karma-runner.github.io/
+[jasmine]: https://jasmine.github.io/
---
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index 74d09eb91ff..0cd63a54b55 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -72,21 +72,6 @@ Everything you should know about how to run end-to-end tests using
---
-## Spinach (feature) tests
-
-GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
-for its feature/integration tests in September 2012.
-
-As of March 2016, we are [trying to avoid adding new Spinach
-tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
-opting for [RSpec feature](#features-integration) specs.
-
-Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
-no more than one new `step` definition. If more than that is required, the
-test should be re-implemented using RSpec instead.
-
----
-
[Return to Development documentation](../README.md)
[^1]: /ci/yaml/README.html#dependencies
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index e86c1f5232a..07ced36f0c1 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -28,7 +28,7 @@ records should use stubs/doubles as much as possible.
| `app/uploaders/` | `spec/uploaders/` | RSpec | |
| `app/views/` | `spec/views/` | RSpec | |
| `app/workers/` | `spec/workers/` | RSpec | |
-| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontent Testing guide](frontend_testing.md) section. |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontend Testing guide](frontend_testing.md) section. |
## Integration tests
@@ -81,7 +81,6 @@ 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]. |
-| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
### Consider **not** writing a system test!
diff --git a/doc/development/testing_guide/testing_rake_tasks.md b/doc/development/testing_guide/testing_rake_tasks.md
index 60163f1a230..db8ca87e9f8 100644
--- a/doc/development/testing_guide/testing_rake_tasks.md
+++ b/doc/development/testing_guide/testing_rake_tasks.md
@@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
-executing tasks simple. See `spec/support/rake_helpers.rb` for all available
+executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available
methods.
Example:
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 012c64be79f..b57520a00e0 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -219,7 +219,7 @@ Blocks are a way to group related information.
#### Content blocks
-Content blocks (`.content-block`) are the basic grouping of content. They are commonly used in [lists](#lists), and are separated by a botton border.
+Content blocks (`.content-block`) are the basic grouping of content. They are commonly used in [lists](#lists), and are separated by a button border.
![Content block](img/components-contentblock.png)
@@ -281,7 +281,7 @@ Modals are only used for having a conversation and confirmation with the user. T
| Modal with 2 actions | Modal with 3 actions | Special confirmation |
| --------------------- | --------------------- | -------------------- |
-| ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![spcial-confirmation](img/modals-special-confimation-dialog.png) |
+| ![two-actions](img/modals-general-confimation-dialog.png) | ![three-actions](img/modals-three-buttons.png) | ![special-confirmation](img/modals-special-confimation-dialog.png) |
> TODO: Special case for modal.
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 9d0c62ecc35..b8be8daa157 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -255,7 +255,7 @@ otherwise it will raise a `TypeError`.
## Adding Indexes
Adding indexes is an expensive process that blocks INSERT and UPDATE queries for
-the duration. When using PostgreSQL one can work arounds this by using the
+the duration. When using PostgreSQL one can work around this by using the
`CONCURRENTLY` option:
```sql
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index d6a13e7483a..1c41fc7611f 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -1,3 +1,7 @@
+---
+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.
@@ -49,7 +53,7 @@ do before.
**Use cases**: provide at least two, ideally three, use cases for every major feature.
You should answer this question: what can you do with this feature/change? Use cases
-are examples of how this feauture or change can be used in real life.
+are examples of how this feature or change can be used in real life.
Examples:
- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
@@ -201,6 +205,19 @@ Things to note:
built-in help page, that's why we omit it in `git grep`.
- Use the checklist on the documentation MR description template.
+#### Alternative redirection method
+
+Alternatively to the method described above, you can simply replace the content
+of the old file with a frontmatter containing a redirect link:
+
+```yaml
+---
+redirect_to: '../path/to/file/README.md'
+---
+```
+
+It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`.
+
### Redirections for pages with Disqus comments
If the documentation page being relocated already has any Disqus comments,
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index f656057e3da..236408762e3 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -15,9 +15,9 @@ Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
you should disable these mechanisms before downgrading and you should provide
alternative authentication methods to your users.
-### Remove Jenkins CI Service entries from the database
+### Remove Service Integration entries from the database
-The `JenkinsService` class is only available on the Enterprise Edition codebase,
+The `JenkinsService` and `GithubService` classes are only available in the Enterprise Edition codebase,
so if you downgrade to the Community Edition, you'll come across the following
error:
@@ -30,20 +30,31 @@ column if you didn't intend it to be used for storing the inheritance class or o
use another column for that information.)
```
+or
+
+```
+Completed 500 Internal Server Error in 497ms (ActiveRecord: 32.2ms)
+
+ActionView::Template::Error (The single-table inheritance mechanism failed to locate the subclass: 'GithubService'. This
+error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this
+column if you didn't intend it to be used for storing the inheritance class or overwrite Service.inheritance_column to
+use another column for that information.)
+```
+
All services are created automatically for every project you have, so in order
to avoid getting this error, you need to remove all instances of the
-`JenkinsService` from your database:
+`JenkinsService` and `GithubService` from your database:
**Omnibus Installation**
```
-$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all"
+$ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
```
**Source Installation**
```
-$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService']).delete_all" production
+$ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
```
### Secret variables environment scopes
diff --git a/doc/install/README.md b/doc/install/README.md
index 5dadf57ea9a..27df03c6ac6 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -1,5 +1,6 @@
---
comments: false
+description: Read through the GitLab installation methods.
---
# Installation
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index b0c3ad960bb..21694b02d18 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -1,3 +1,8 @@
+---
+description: 'Learn how to spin up a
+pre-configured GitLab VM on Microsoft Azure and have your very own private GitLab instance up and running in around 30 minutes.'
+---
+
# Install GitLab on Microsoft Azure
> _This article was originally written by Dave Wentzel and [published on the GitLab Blog][Original-Blog-Post]._
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index 5c7557ed2b3..e1af086f418 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -91,7 +91,7 @@ Follow the below instructions to ensure you use the most up to date requirements
#### Check for InnoDB File-Per-Table Tablespaces
-We need to check, enable and maybe convert your existing GitLab DB tables to the [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) as a prerequise for supporting **utfb8mb4 with long indexes** required by recent GitLab databases.
+We need to check, enable and maybe convert your existing GitLab DB tables to the [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) as a prerequisite for supporting **utfb8mb4 with long indexes** required by recent GitLab databases.
# Login to MySQL
mysql -u root -p
diff --git a/doc/install/docker.md b/doc/install/docker.md
index 933756072ff..c7dc9db70c5 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -1,4 +1,4 @@
-# GitLab Docker images
+# Install GitLab with Docker
[Docker](https://www.docker.com) and container technology have been revolutionizing the software world for the past few years. They combine the performance and efficiency of native execution with the abstraction, security, and immutability of virtualization.
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
index 3389f0260f9..ab5f7507f24 100644
--- a/doc/install/google_cloud_platform/index.md
+++ b/doc/install/google_cloud_platform/index.md
@@ -1,8 +1,12 @@
+---
+description: 'Learn how to install a GitLab instance on Google Cloud Platform.'
+---
+
# Installing GitLab on Google Cloud Platform
![GCP landing page](img/gcp_landing.png)
-Gettung started with GitLab on a [Google Cloud Platform (GCP)][gcp] instance is quick and easy.
+Getting started with GitLab on a [Google Cloud Platform (GCP)][gcp] instance is quick and easy.
## Prerequisites
diff --git a/doc/install/installation.md b/doc/install/installation.md
index fa5bcfa6f07..a0ae9017f71 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -301,9 +301,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 10-7-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-8-stable gitlab
-**Note:** You can change `10-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `10-8-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 84eeacac3fd..429519a92e1 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,488 +1,6 @@
# GitLab Helm Chart
-> **Note:**
-* This chart has been tested on Google Kubernetes Engine and Azure Container Service.
+> **Note:** This chart is currently in alpha.
-**This chart is deprecated.** For small installations on Kubernetes today, we recommend the beta [`gitlab-omnibus` Helm chart](gitlab_omnibus.md).
+The cloud native `gitlab` chart is the next generation Helm chart, currently in alpha, and will replace the [`gitlab-omnibus`](gitlab_omnibus.md) chart. It will support large deployments with horizontal scaling of individual GitLab components.
-A new [cloud native GitLab chart](index.md#cloud-native-gitlab-chart) is in development with increased scalability and resilience, among other benefits. The cloud native chart will replace both the `gitlab` and `gitlab-omnibus` charts when available later this year.
-
-Due to the significant architectural changes, migrating will require backing up data out of this instance and restoring it into the new deployment. For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
-
-## Introduction
-
-The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. We **strongly recommend** the [gitlab-omnibus](gitlab_omnibus.md) chart.
-
-This chart includes the following:
-
-- Deployment using the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce) or [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee) container image
-- ConfigMap containing the `gitlab.rb` contents that configure [Omnibus GitLab](https://docs.gitlab.com/omnibus/settings/configuration.html#configuration-options)
-- Persistent Volume Claims for Data, Config, Logs, and Registry Storage
-- A Kubernetes service
-- Optional Redis deployment using the [Redis Chart](https://github.com/kubernetes/charts/tree/master/stable/redis) (defaults to enabled)
-- Optional PostgreSQL deployment using the [PostgreSQL Chart](https://github.com/kubernetes/charts/tree/master/stable/postgresql) (defaults to enabled)
-- Optional Ingress (defaults to disabled)
-
-## Prerequisites
-
-- _At least_ 3 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required.
-- Kubernetes 1.4+ with Beta APIs enabled
-- [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure
-- The ability to point a DNS entry or URL at your GitLab install
-- The `kubectl` CLI installed locally and authenticated for the cluster
-- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine
-
-## Configuring GitLab
-
-Create a `values.yaml` file for your GitLab configuration. See the
-[Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md)
-for information on how your values file will override the defaults.
-
-The default configuration can always be [found in the `values.yaml`](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab/values.yaml), in the chart repository.
-
-### Required configuration
-
-In order for GitLab to function, your config file **must** specify the following:
-
-- An `externalUrl` that GitLab will be reachable at.
-
-### Choosing GitLab Edition
-
-The Helm chart defaults to installing GitLab CE. This can be controlled by setting the `edition` variable in your values.
-
-Setting `edition` to GitLab Enterprise Edition (EE) in your `values.yaml`
-
-```yaml
-edition: EE
-
-externalUrl: 'http://gitlab.example.com'
-```
-
-### Choosing a different GitLab release version
-
-The version of GitLab installed is based on the `edition` setting (see [section](#choosing-gitlab-edition) above), and
-the value of the corresponding helm setting: `ceImage` or `eeImage`.
-
-```yaml
-## GitLab Edition
-## ref: https://about.gitlab.com/products/
-## - CE - Community Edition
-## - EE - Enterprise Edition - (requires license issued by GitLab Inc)
-##
-edition: CE
-
-## GitLab CE image
-## ref: https://hub.docker.com/r/gitlab/gitlab-ce/tags/
-##
-ceImage: gitlab/gitlab-ce:9.1.2-ce.0
-
-## GitLab EE image
-## ref: https://hub.docker.com/r/gitlab/gitlab-ee/tags/
-##
-eeImage: gitlab/gitlab-ee:9.1.2-ee.0
-```
-
-The different images can be found in the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce/tags/) and [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee/tags/)
-repositories on Docker Hub
-
-> **Note:**
-There is no guarantee that other release versions of GitLab, other than what are
-used by default in the chart, will be supported by a chart install.
-
-
-### Custom Omnibus GitLab configuration
-
-In addition to the configuration options provided for GitLab in the Helm Chart, you can also pass any custom configuration
-that is valid for the [Omnibus GitLab Configuration](https://docs.gitlab.com/omnibus/settings/configuration.html).
-
-The setting to pass these values in is `omnibusConfigRuby`. It accepts any valid
-Ruby code that could used in the Omnibus `/etc/gitlab/gitlab.rb` file. In
-Kubernetes, the contents will be stored in a ConfigMap.
-
-Example setting:
-
-```yaml
-omnibusConfigRuby: |
- unicorn['worker_processes'] = 2;
- gitlab_rails['trusted_proxies'] = ["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"];
-```
-
-### Persistent storage
-
-By default, persistent storage is enabled for GitLab and the charts it depends
-on (Redis and PostgreSQL).
-
-Components can have their claim size set from your `values.yaml`, and each
-component allows you to optionally configure the `storageClass` variable so you
-can take advantage of faster drives on your cloud provider.
-
-Basic configuration:
-
-```yaml
-## Enable persistence using Persistent Volume Claims
-## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
-## ref: https://docs.gitlab.com/ce/install/requirements.html#storage
-##
-persistence:
- ## This volume persists generated configuration files, keys, and certs.
- ##
- gitlabEtc:
- enabled: true
- size: 1Gi
- ## If defined, volume.beta.kubernetes.io/storage-class: <storageClass>
- ## Default: volume.alpha.kubernetes.io/storage-class: default
- ##
- # storageClass:
- accessMode: ReadWriteOnce
- ## This volume is used to store git data and other project files.
- ## ref: https://docs.gitlab.com/omnibus/settings/configuration.html#storing-git-data-in-an-alternative-directory
- ##
- gitlabData:
- enabled: true
- size: 10Gi
- ## If defined, volume.beta.kubernetes.io/storage-class: <storageClass>
- ## Default: volume.alpha.kubernetes.io/storage-class: default
- ##
- # storageClass:
- accessMode: ReadWriteOnce
- gitlabRegistry:
- enabled: true
- size: 10Gi
- ## If defined, volume.beta.kubernetes.io/storage-class: <storageClass>
- ## Default: volume.alpha.kubernetes.io/storage-class: default
- ##
- # storageClass:
-
- postgresql:
- persistence:
- # storageClass:
- size: 10Gi
- ## Configuration values for the Redis dependency.
- ## ref: https://github.com/kubernetes/charts/blob/master/stable/redis/README.md
- ##
- redis:
- persistence:
- # storageClass:
- size: 10Gi
-```
-
->**Note:**
-You can make use of faster SSD drives by adding a [StorageClass] to your cluster
-and using the `storageClass` setting in the above config to the name of
-your new storage class.
-
-### Routing
-
-By default, the GitLab chart uses a service type of `LoadBalancer` which will
-result in the GitLab service being exposed externally using your cloud provider's
-load balancer.
-
-This field is configurable in your `values.yml` by setting the top-level
-`serviceType` field. See the [Service documentation][kube-srv] for more
-information on the possible values.
-
-#### Ingress routing
-
-Optionally, you can enable the Chart's ingress for use by an ingress controller
-deployed in your cluster.
-
-To enable the ingress, edit its section in your `values.yaml`:
-
-```yaml
-ingress:
- ## If true, gitlab Ingress will be created
- ##
- enabled: true
-
- ## gitlab Ingress hostnames
- ## Must be provided if Ingress is enabled
- ##
- hosts:
- - gitlab.example.com
-
- ## gitlab Ingress annotations
- ##
- annotations:
- kubernetes.io/ingress.class: nginx
-```
-
-You must also provide the list of hosts that the ingress will use. In order for
-you ingress controller to work with the GitLab Ingress, you will need to specify
-its class in an annotation.
-
->**Note:**
-The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
-Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
-to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
->**Note:**
-If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
-
-#### Preserving Source IPs
-
-If you are using the `LoadBalancer` serviceType you may run into issues where user IP addresses in the GitLab
-logs, and used in abuse throttling are not accurate. This is due to how Kubernetes uses source NATing on cluster nodes without endpoints.
-
-See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) for more information.
-
-To fix this you can add the following service annotation to your `values.yaml`
-
-```yaml
-## For minikube, set this to NodePort, elsewhere use LoadBalancer
-## ref: http://kubernetes.io/docs/user-guide/services/#publishing-services---service-types
-##
-serviceType: LoadBalancer
-
-## Optional annotations for gitlab service.
-serviceAnnotations:
- service.beta.kubernetes.io/external-traffic: "OnlyLocal"
-```
-
->**Note:**
-If you are using the ingress routing, you will likely also need to specify the annotation on the service for the ingress
-controller. For `nginx-ingress` you can check the
-[configuration documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md#configuration)
-on how to add the annotation to the `controller.service.annotations` array.
-
->**Note:**
-When using the `nginx-ingress` controller on Google Kubernetes Engine (GKE), and using the `external-traffic` annotation,
-you will need to additionally set the `controller.kind` to be DaemonSet. Otherwise only pods running on the same node
-as the nginx controller will be able to reach GitLab. This may result in pods within your cluster not being able to reach GitLab.
-See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) and
-[nginx-ingress configuration documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md#configuration)
-for more information.
-
-### External database
-
-You can configure the GitLab Helm chart to connect to an external PostgreSQL
-database.
-
->**Note:**
-This is currently our recommended approach for a Production setup.
-
-To use an external database, in your `values.yaml`, disable the included
-PostgreSQL dependency, then configure access to your database:
-
-```yaml
-dbHost: "<reachable postgres hostname>"
-dbPassword: "<password for the user with access to the db>"
-dbUsername: "<user with read/write access to the database>"
-dbDatabase: "<database name on postgres to connect to for GitLab>"
-
-postgresql:
- # Sets whether the PostgreSQL helm chart is used as a dependency
- enabled: false
-```
-
-Be sure to check the GitLab documentation on how to
-[configure the external database](../requirements.md#postgresql-requirements)
-
-You can also configure the chart to use an external Redis server, but this is
-not required for basic production use:
-
-```yaml
-dbHost: "<reachable redis hostname>"
-dbPassword: "<password>"
-
-redis:
- # Sets whether the Redis helm chart is used as a dependency
- enabled: false
-```
-
-### Sending email
-
-By default, the GitLab container will not be able to send email from your cluster.
-In order to send email, you should configure SMTP settings in the
-`omnibusConfigRuby` section, as per the [GitLab Omnibus documentation](https://docs.gitlab.com/omnibus/settings/smtp.html).
-
->**Note:**
-Some cloud providers restrict emails being sent out on SMTP, so you will have
-to use a SMTP service that is supported by your provider. See this
-[Google Cloud Platform page](https://cloud.google.com/compute/docs/tutorials/sending-mail/)
-as and example.
-
-Here is an example configuration for Mailgun SMTP support:
-
-```yaml
-omnibusConfigRuby: |
- # This is example config of what you may already have in your omnibusConfigRuby object
- unicorn['worker_processes'] = 2;
- gitlab_rails['trusted_proxies'] = ["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"];
-
- # SMTP settings
- gitlab_rails['smtp_enable'] = true
- gitlab_rails['smtp_address'] = "smtp.mailgun.org"
- gitlab_rails['smtp_port'] = 2525 # High port needed for Google Cloud
- gitlab_rails['smtp_authentication'] = "plain"
- gitlab_rails['smtp_enable_starttls_auto'] = false
- gitlab_rails['smtp_user_name'] = "postmaster@mg.your-mail-domain"
- gitlab_rails['smtp_password'] = "you-password"
- gitlab_rails['smtp_domain'] = "mg.your-mail-domain"
-```
-
-### HTTPS configuration
-
-To setup HTTPS access to your GitLab server, first you need to configure the
-chart to use the [ingress](#ingress-routing).
-
-GitLab's config should be updated to support [proxied SSL](https://docs.gitlab.com/omnibus/settings/nginx.html#supporting-proxied-ssl).
-
-In addition to having a Ingress Controller deployed and the basic ingress
-settings configured, you will also need to specify in the ingress settings
-which hosts to use HTTPS for.
-
-Make sure `externalUrl` now includes `https://` instead of `http://` in its
-value, and update the `omnibusConfigRuby` section:
-
-```yaml
-externalUrl: 'https://gitlab.example.com'
-
-omnibusConfigRuby: |
- # This is example config of what you may already have in your omnibusConfigRuby object
- unicorn['worker_processes'] = 2;
- gitlab_rails['trusted_proxies'] = ["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"];
-
- # These are the settings needed to support proxied SSL
- nginx['listen_port'] = 80
- nginx['listen_https'] = false
- nginx['proxy_set_headers'] = {
- "X-Forwarded-Proto" => "https",
- "X-Forwarded-Ssl" => "on"
- }
-
-ingress:
- enabled: true
- annotations:
- kubernetes.io/ingress.class: nginx
- # kubernetes.io/tls-acme: 'true' Annotation used for letsencrypt support
-
- hosts:
- - gitlab.example.com
-
- ## gitlab Ingress TLS configuration
- ## Secrets must be created in the namespace, and is not done for you in this chart
- ##
- tls:
- - secretName: gitlab-tls
- hosts:
- - gitlab.example.com
-```
-
-You will need to create the named secret in your cluster, specifying the private
-and public certificate pair using the format outlined in the
-[ingress documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#tls).
-
-Alternatively, you can use the `kubernetes.io/tls-acme` annotation, and install
-the `kube-lego` chart to your cluster to have Let's Encrypt issue your
-certificate. See the [kube-lego documentation](https://github.com/kubernetes/charts/blob/master/stable/kube-lego/README.md)
-for more information.
-
-### Enabling the GitLab Container Registry
-
-The GitLab Registry is disabled by default but can be enabled by providing an
-external URL for it in the configuration. In order for the Registry to be easily
-used by GitLab CI and your Kubernetes cluster, you will need to set it up with
-a TLS certificate, so these examples will include the ingress settings for that
-as well. See the [HTTPS Configuration section](#https-configuration)
-for more explanation on some of these settings.
-
-Example config:
-
-```yaml
-externalUrl: 'https://gitlab.example.com'
-
-omnibusConfigRuby: |
- # This is example config of what you may already have in your omnibusConfigRuby object
- unicorn['worker_processes'] = 2;
- gitlab_rails['trusted_proxies'] = ["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"];
-
- registry_external_url 'https://registry.example.com';
-
- # These are the settings needed to support proxied SSL
- nginx['listen_port'] = 80
- nginx['listen_https'] = false
- nginx['proxy_set_headers'] = {
- "X-Forwarded-Proto" => "https",
- "X-Forwarded-Ssl" => "on"
- }
- registry_nginx['listen_port'] = 80
- registry_nginx['listen_https'] = false
- registry_nginx['proxy_set_headers'] = {
- "X-Forwarded-Proto" => "https",
- "X-Forwarded-Ssl" => "on"
- }
-
-ingress:
- enabled: true
- annotations:
- kubernetes.io/ingress.class: nginx
- # kubernetes.io/tls-acme: 'true' Annotation used for letsencrypt support
-
- hosts:
- - gitlab.example.com
- - registry.example.com
-
- ## gitlab Ingress TLS configuration
- ## Secrets must be created in the namespace, and is not done for you in this chart
- ##
- tls:
- - secretName: gitlab-tls
- hosts:
- - gitlab.example.com
- - registry.example.com
-```
-
-## Installing GitLab using the Helm Chart
-> You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` while storage provisions. Once the storage provisions, the pods will automatically restart. This may take a couple minutes depending on your cloud provider. If the error persists, please review the [prerequisites](#prerequisites) to ensure you have enough RAM, CPU, and storage.
-
-Add the GitLab Helm repository and initialize Helm:
-
-```bash
-helm repo add gitlab https://charts.gitlab.io
-helm init
-```
-
-Once you [have configured](#configuration) GitLab in your `values.yml` file,
-run the following:
-
-```bash
-helm install --namespace <NAMESPACE> --name gitlab -f <CONFIG_VALUES_FILE> gitlab/gitlab
-```
-
-where:
-
-- `<NAMESPACE>` is the Kubernetes namespace where you want to install GitLab.
-- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom
- configuration. See the [Configuration](#configuration) section to create it.
-
-## Updating GitLab using the Helm Chart
-
-Once your GitLab Chart is installed, configuration changes and chart updates
-should we done using `helm upgrade`
-
-```bash
-helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab
-```
-
-where:
-
-- `<NAMESPACE>` is the Kubernetes namespace where GitLab is installed.
-- `<CONFIG_VALUES_FILE>` is the path to values file containing your custom
- [configuration] (#configuration).
-- `<RELEASE-NAME>` is the name you gave the chart when installing it.
- In the [Install section](#installing) we called it `gitlab`.
-
-## Uninstalling GitLab using the Helm Chart
-
-To uninstall the GitLab Chart, run the following:
-
-```bash
-helm delete --namespace <NAMESPACE> <RELEASE-NAME>
-```
-
-where:
-
-- `<NAMESPACE>` is the Kubernetes namespace where GitLab is installed.
-- `<RELEASE-NAME>` is the name you gave the chart when installing it.
- In the [Install section](#installing) we called it `gitlab`.
-
-[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
-[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
+Installation instructions and known issues during alpha are available at the [project page](https://gitlab.com/charts/gitlab/). \ No newline at end of file
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 9c5258c2cdf..98af87455ec 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -129,8 +129,8 @@ You may see a temporary error message `SchedulerPredicates failed due to Persist
Add the GitLab Helm repository and initialize Helm:
```bash
-helm repo add gitlab https://charts.gitlab.io
helm init
+helm repo add gitlab https://charts.gitlab.io
```
Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab) you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future.
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index a03c49cbd89..2aab225fcdb 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/charts.gitlab.io/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/charts/gitlab-runner/issues).
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster.
@@ -25,7 +25,7 @@ For more information on available GitLab Helm Charts, please see our [overview](
Create a `values.yaml` file for your GitLab Runner configuration. See [Helm docs](https://github.com/kubernetes/helm/blob/master/docs/chart_template_guide/values_files.md)
for information on how your values file will override the defaults.
-The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository.
+The default configuration can always be found in the [values.yaml](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository.
### Required configuration
@@ -39,7 +39,7 @@ Unless you need to specify additional configuration, you are [ready to install](
### Other configuration
-The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository.
+The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/gitlab-runner/blob/master/values.yaml) in the chart repository.
Here is a snippet of the important settings:
@@ -50,12 +50,12 @@ Here is a snippet of the important settings:
gitlabUrl: http://gitlab.your-domain.com/
## The Registration Token for adding new Runners to the GitLab Server. This must
-## be retreived from your GitLab Instance.
+## be retrieved from your GitLab Instance.
## ref: https://docs.gitlab.com/ce/ci/runners/README.html#creating-and-registering-a-runner
##
runnerRegistrationToken: ""
-## Set the certsSecretName in order to pass custom certficates for GitLab Runner to use
+## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use
## Provide resource name for a Kubernetes Secret Object in the same namespace,
## this is used to populate the /etc/gitlab-runner/certs directory
## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates
@@ -130,7 +130,7 @@ runners:
### Enabling RBAC support
-If your cluster has RBAC enabled, you can choose to either have the chart create its own sevice account or provide one.
+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.
@@ -208,7 +208,7 @@ You then need to provide the secret's name to the GitLab Runner chart.
Add the following to your `values.yaml`
```yaml
-## Set the certsSecretName in order to pass custom certficates for GitLab Runner to use
+## Set the certsSecretName in order to pass custom certificates for GitLab Runner to use
## Provide resource name for a Kubernetes Secret Object in the same namespace,
## this is used to populate the /etc/gitlab-runner/certs directory
## ref: https://docs.gitlab.com/runner/configuration/tls-self-signed.html#supported-options-for-self-signed-certificates
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index aa9b8777359..aeaa739edab 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,4 +1,9 @@
+---
+description: 'Read through the different methods to deploy GitLab on Kubernetes.'
+---
+
# Installing GitLab on Kubernetes
+
> **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/charts.gitlab.io/issues).
The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
@@ -10,13 +15,13 @@ should be deployed, upgraded, and configured.
## Chart Overview
* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
-* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in alpha. Will support large deployments with horizontal scaling of individual GitLab components.
+* **[Cloud Native GitLab Chart](https://gitlab.com/charts/gitlab/blob/master/README.md)**: The next generation GitLab chart, currently in alpha. Will support large deployments with horizontal scaling of individual GitLab components.
* Other Charts
* [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
- * [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
## GitLab-Omnibus Chart (Recommended)
+
> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added.
This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
@@ -27,7 +32,7 @@ Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
## Cloud Native GitLab Chart
-GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended).
+GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/gitlab/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended).
By offering individual containers and charts, we will be able to provide a number of benefits:
* Easier horizontal scaling of each service,
@@ -37,7 +42,7 @@ By offering individual containers and charts, we will be able to provide a numbe
Presently this chart is available in alpha for testing, and not recommended for production use.
-Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
+Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/gitlab/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
## Other Charts
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index e6ccfccd33f..1ced1fb513d 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -6,7 +6,7 @@ article_type: tutorial
date: 2016-06-28
---
-# Getting started with OpenShift Origin 3 and GitLab
+# How to install GitLab on OpenShift Origin 3
## Introduction
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 1f2b4d9d3d9..1f399a8a3f7 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -27,8 +27,8 @@ Please see the [installation from source guide](installation.md) and the [instal
### Non-Unix operating systems such as Windows
-GitLab is developed for Unix operating systems.
-GitLab does **not** run on Windows and we have no plans of supporting it in the near future.
+GitLab is developed for Unix operating systems.
+It does **not** run on Windows, and we have no plans to support it in the near future. For the latest development status view this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/46567).
Please consider using a virtual machine to run GitLab.
## Ruby versions
diff --git a/doc/integration/github.md b/doc/integration/github.md
index b0d67db8b59..23bb8ef9303 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -69,7 +69,7 @@ GitHub will generate an application ID and secret key for you to use.
"name" => "github",
"app_id" => "YOUR_APP_ID",
"app_secret" => "YOUR_APP_SECRET",
- "url" => "https://github.com/",
+ "url" => "https://github.example.com/",
"args" => { "scope" => "user:email" }
}
]
@@ -125,7 +125,7 @@ For omnibus package:
"name" => "github",
"app_id" => "YOUR_APP_ID",
"app_secret" => "YOUR_APP_SECRET",
- "url" => "https://github.com/",
+ "url" => "https://github.example.com/",
"verify_ssl" => false,
"args" => { "scope" => "user:email" }
}
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index e0fc1bb801f..8611d4f7315 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -43,7 +43,7 @@ exclude shibboleth URLs from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibbo
RequestHeader set X_FORWARDED_PROTO 'https'
```
-1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should addjust them to your need and environment. Add any other configuration you need.
+1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should adjust them to your need and environment. Add any other configuration you need.
File should look like this:
```
diff --git a/doc/legal/README.md b/doc/legal/README.md
index 6413f1d645f..d991429a652 100644
--- a/doc/legal/README.md
+++ b/doc/legal/README.md
@@ -4,5 +4,4 @@ comments: false
# Legal
-- [Corporate contributor license agreement](corporate_contributor_license_agreement.md)
-- [Individual contributor license agreement](individual_contributor_license_agreement.md)
+Please read through the [GitLab License Agreement](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md).
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index ebb24ba0a7f..e5fc7a3c85f 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -1,2 +1,3 @@
-This document has been replaced by a Developer Certificate of Origin and License,
-as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
+---
+redirect_to: 'README.md'
+---
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index ebb24ba0a7f..e5fc7a3c85f 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -1,2 +1,3 @@
-This document has been replaced by a Developer Certificate of Origin and License,
-as described in [Contributing.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). \ No newline at end of file
+---
+redirect_to: 'README.md'
+---
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index bbd2d214fe4..5faae7ca2d6 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -31,8 +31,8 @@ sudo apt-get install -y rsync
>**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_2017_04_25`
-would become `1493107454_2017_04_25_9.1.0`.
+`EPOCH_YYYY_MM_DD_GitLab_version`, for example `1493107454_2018_04_25`
+would become `1493107454_2018_04_25_10.6.4-ce`.
The backup archive will be saved in `backup_path`, which is specified in the
`config/gitlab.yml` file.
@@ -41,8 +41,8 @@ identifies the time at which each backup was created, plus the GitLab version.
The timestamp is needed if you need to restore GitLab and multiple backups are
available.
-For example, if the backup name is `1493107454_2017_04_25_9.1.0_gitlab_backup.tar`,
-then the timestamp is `1493107454_2017_04_25_9.1.0`.
+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
@@ -64,6 +64,13 @@ If you are running GitLab within a Docker container, you can run the backup from
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
+
+```
+kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:create
+```
+
Example output:
```
@@ -498,6 +505,13 @@ more of the following options:
Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed.
+If you are restoring into directories that are mountpoints you will need to make
+sure these directories are empty before attempting a restore. Otherwise GitLab
+will attempt to move these directories before restoring the new data and this
+would cause an error.
+
+Read more on [configuring NFS mounts](../administration/high_availability/nfs.md)
+
### Restore for installation from source
```
@@ -560,7 +574,7 @@ First make sure your backup tar file is in the backup directory described in the
`/var/opt/gitlab/backups`.
```shell
-sudo cp 1493107454_2017_04_25_9.1.0_gitlab_backup.tar /var/opt/gitlab/backups/
+sudo cp 11493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar /var/opt/gitlab/backups/
```
Stop the processes that are connected to the database. Leave the rest of GitLab
@@ -578,7 +592,7 @@ restore:
```shell
# This command will overwrite the contents of your GitLab database!
-sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0
+sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2018_04_25_10.6.4-ce
```
Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above.
@@ -594,6 +608,34 @@ If there is a GitLab version mismatch between your backup tar file and the insta
version of GitLab, the restore command will abort with an error. Install the
[correct GitLab version](https://packages.gitlab.com/gitlab/) and try again.
+### Restore for Docker image and gitlab-omnibus helm chart
+
+For GitLab installations using docker image or the gitlab-omnibus helm chart on
+a Kubernetes cluster, restore task expects the restore directories to be empty.
+However, with docker and Kubernetes volume mounts, some system level directories
+may be created at the volume roots, like `lost+found` directory found in Linux
+operating systems. These directories are usually owned by `root`, which can
+cause access permission errors since the restore rake task runs as `git` user.
+So, to restore a GitLab installation, users have to confirm the restore target
+directories are empty.
+
+For both these installation types, the backup tarball has to be available in the
+backup location (default location is `/var/opt/gitlab/backups`).
+
+For docker installations, the restore task can be run from host using the
+command
+
+```
+docker exec -it <name of container> gitlab-rake gitlab:backup:restore
+```
+
+Similarly, for gitlab-omnibus helm chart, the restore task can be run on the
+gitlab application pod using kubectl
+
+```
+kubectl exec -it <gitlab-gitlab pod> gitlab-rake gitlab:backup:restore
+```
+
## Alternative backup strategies
If your GitLab server contains a lot of Git repository data you may find the GitLab backup script to be too slow.
diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md
index fee49cc27cc..57c16f110e4 100644
--- a/doc/raketasks/features.md
+++ b/doc/raketasks/features.md
@@ -1,4 +1,4 @@
-# Features
+# Namespaces
## Enable usernames and namespaces for user projects
diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md
index 2ebf7c48f4e..5f3143f76cd 100644
--- a/doc/raketasks/web_hooks.md
+++ b/doc/raketasks/web_hooks.md
@@ -1,4 +1,4 @@
-# Webhooks
+# Webhooks administration **[CORE ONLY]**
## Add a webhook for **ALL** projects:
diff --git a/doc/security/img/outbound_requests_section.png b/doc/security/img/outbound_requests_section.png
new file mode 100644
index 00000000000..95c9c6ee771
--- /dev/null
+++ b/doc/security/img/outbound_requests_section.png
Binary files differ
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index faabc53ce72..a573445ab5b 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -2,12 +2,19 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
-If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
+If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
-To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
+To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed.
+
+This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`):
+
+![Outbound requests admin settings](img/outbound_requests_section.png)
+
+>**Note:**
+*System hooks* are exempt from this protection because they are set up by admins.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index aa14a39e4c9..b71e9bf3000 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -196,7 +196,7 @@ This is really useful for integrating repositories to secured, shared Continuous
Integration (CI) services or other shared services.
GitLab administrators can set up the Global Shared Deploy key in GitLab and
add the private key to any shared systems. Individual repositories opt into
-exposing their repsitory using these keys when a project masters (or higher)
+exposing their repository using these keys when a project masters (or higher)
authorizes a Global Shared Deploy key to be used with their project.
Global Shared Keys can provide greater security compared to Per-Project Deploy
@@ -224,7 +224,7 @@ if there is at least one Global Deploy Key configured.
CAUTION: **Warning:**
Defining Global Deploy Keys does not expose any given repository via
-the key until that respository adds the Global Deploy Key to their project.
+the key until that repository adds the Global Deploy Key to their project.
In this way the Global Deploy Keys enable access by other systems, but do
not implicitly give any access just by setting them up.
diff --git a/doc/topics/autodevops/img/rollout_enabled.png b/doc/topics/autodevops/img/rollout_enabled.png
new file mode 100644
index 00000000000..d6e7d790cf2
--- /dev/null
+++ b/doc/topics/autodevops/img/rollout_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_disabled.png b/doc/topics/autodevops/img/rollout_staging_disabled.png
new file mode 100644
index 00000000000..71e36b440f0
--- /dev/null
+++ b/doc/topics/autodevops/img/rollout_staging_disabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/rollout_staging_enabled.png b/doc/topics/autodevops/img/rollout_staging_enabled.png
new file mode 100644
index 00000000000..d0d1d356627
--- /dev/null
+++ b/doc/topics/autodevops/img/rollout_staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/img/staging_enabled.png b/doc/topics/autodevops/img/staging_enabled.png
new file mode 100644
index 00000000000..0ef1a67d641
--- /dev/null
+++ b/doc/topics/autodevops/img/staging_enabled.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index fb2ce27bf49..efec365042a 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -1,7 +1,5 @@
# Auto DevOps
-DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_.
-
> [Introduced][ce-37115] in GitLab 10.0.
Auto DevOps automatically detects, builds, tests, deploys, and monitors your
@@ -10,8 +8,30 @@ applications.
## Overview
With Auto DevOps, the software development process becomes easier to set up
-as every project can have a complete workflow from build to deploy and monitoring,
-with minimal to zero configuration.
+as every project can have a complete workflow from verification to monitoring
+without needing to configure anything. Just push your code and GitLab takes
+care of everything else. This makes it easier to start new projects and brings
+consistency to how applications are set up throughout a company.
+
+## Comparison to application platforms and PaaS
+
+Auto DevOps provides functionality described by others as an application
+platform or as a Platform as a Service (PaaS). It takes inspiration from the
+innovative work done by [Heroku](https://www.heroku.com/) and goes beyond it
+in a couple of ways:
+
+1. Auto DevOps works with any Kubernetes cluster, you're not limited to running
+ on GitLab's infrastructure (note that many features also work without Kubernetes).
+1. There is no additional cost (no markup on the infrastructure costs), and you
+ can use a self-hosted Kubernetes cluster or Containers as a Service on any
+ public cloud (for example [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)).
+1. Auto DevOps has more features including security testing, performance testing,
+ and code quality testing.
+1. It offers an incremental graduation path. If you need advanced customizations
+ you can start modifying the templates without having to start over on a
+ completely different platform.
+
+## Features
Comprised of a set of stages, Auto DevOps brings these best practices to your
project in an easy and automatic way:
@@ -113,6 +133,11 @@ and `1.2.3.4` is the IP address of your load balancer; generally NGINX
([see prerequisites](#prerequisites)). How to set up the DNS record is beyond
the scope of this document; you should check with your DNS provider.
+Alternatively you can use free public services like [xip.io](http://xip.io) or
+[nip.io](http://nip.io) which provide automatic wildcard DNS without any
+configuration. Just set the Auto DevOps base domain to `1.2.3.4.xip.io` or
+`1.2.3.4.nip.io`.
+
Once set up, all requests will hit the load balancer, which in turn will route
them to the Kubernetes pods that run your application(s).
@@ -195,8 +220,8 @@ tests, it's up to you to add them.
### Auto Code Quality
-Auto Code Quality uses the open source
-[`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run
+Auto Code Quality uses the
+[Code Quality image](https://gitlab.com/gitlab-org/security-products/codequality) to run
static analysis and other code checks on the current code. The report is
created, and is uploaded as an artifact which you can later download and check
out.
@@ -362,7 +387,7 @@ If you have installed GitLab using a different method, you need to:
1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
1. If you would like response metrics, ensure you are running at least version
0.9.0 of NGINX Ingress and
- [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
+ [enable Prometheus metrics](https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/customization/custom-vts-metrics-prometheus/nginx-vts-metrics-conf.yaml).
1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)
the NGINX Ingress deployment to be scraped by Prometheus using
`prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
@@ -468,6 +493,17 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
+| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
+| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
+| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. |
+| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. |
+| `CODEQUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. |
+| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. |
+| `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. |
+| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. |
+| `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. |
TIP: **Tip:**
Set up the replica variables using a
@@ -534,6 +570,88 @@ service:
internalPort: 5000
```
+#### Deploy policy for staging and production environments
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/160)
+in GitLab 10.8.
+
+The normal behavior of Auto DevOps is to use Continuous Deployment, pushing
+automatically to the `production` environment every time a new pipeline is run
+on the default branch. However, there are cases where you might want to use a
+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
+to a `staging` environment, and a `production_manual` job will be created for
+you when you're ready to manually deploy to production.
+
+#### Deploy policy for canary environments **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/171)
+in GitLab 11.0.
+
+A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployments.html) can be used
+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:
+
+- `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
+ deploy to production.
+
+#### Incremental rollout to production **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8.
+
+When you have a new version of your app to deploy in production, you may want
+to use an incremental rollout to replace just a few pods with the latest code.
+This will allow you to first check how the app is behaving, and later manually
+increasing the rollout up to 100%.
+
+If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set
+`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the
+standard `production` job, 4 different
+[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph)
+will be created:
+
+1. `rollout 10%`
+1. `rollout 25%`
+1. `rollout 50%`
+1. `rollout 100%`
+
+The percentage is based on the `REPLICAS` variable and defines the number of
+pods you want to have for your deployment. If you say `10`, and then you run
+the `10%` rollout job, there will be `1` new pod + `9` old ones.
+
+To start a job, click on the play icon next to the job's name. You are not
+required to go from `10%` to `100%`, you can jump to whatever job you want.
+You can also scale down by running a lower percentage job, just before hitting
+`100%`. Once you get to `100%`, you cannot scale down, and you'd have to roll
+back by redeploying the old version using the
+[rollback button](../../ci/environments.md#rolling-back-changes) in the
+environment page.
+
+Below, you can see how the pipeline will look if the rollout or staging
+variables are defined.
+
+- **Without `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`**
+
+ ![Staging and rollout disabled](img/rollout_staging_disabled.png)
+
+- **Without `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`**
+
+ ![Staging enabled](img/staging_enabled.png)
+
+- **With `INCREMENTAL_ROLLOUT_ENABLED` and without `STAGING_ENABLED`**
+
+ ![Rollout enabled](img/rollout_enabled.png)
+
+- **With `INCREMENTAL_ROLLOUT_ENABLED` and with `STAGING_ENABLED`**
+
+ ![Rollout and staging enabled](img/rollout_staging_enabled.png)
+
## Currently supported languages
NOTE: **Note:**
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 15567715c98..61c04f3d9bb 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -1,7 +1,5 @@
# Auto DevOps: quick start guide
-DANGER: Auto DevOps is currently in **Beta** and _not recommended for production use_.
-
> [Introduced][ce-37115] in GitLab 10.0.
This is a step-by-step guide to deploying a project hosted on GitLab.com to
@@ -23,6 +21,10 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
**Fork** button. Soon you should have a project under your namespace with the
necessary files.
+You can also start a new project from a
+[GitLab project template](https://gitlab.com/gitlab-org/project-templates) if
+you want to use a different language.
+
## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at
@@ -124,10 +126,10 @@ Next, a pipeline needs to be triggered. Since the test project doesn't have a
manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/new`,
where `<username>` is your username.
-This will create a new pipeline with several jobs: `build`, `test`, `codequality`,
+This will create a new pipeline with several jobs: `build`, `test`, `code_quality`,
and `production`. The `build` job will create a Docker image with your new
change and push it to the Container Registry. The `test` job will test your
-changes, whereas the `codequality` job will run static analysis on your changes.
+changes, whereas the `code_quality` job will run static analysis on your changes.
Finally, the `production` job will deploy your changes to a production application.
Once the deploy job succeeds you should be able to see your application by
diff --git a/doc/topics/git/how_to_install_git/index.md b/doc/topics/git/how_to_install_git/index.md
index 6c909a1ba86..58d86f7d387 100644
--- a/doc/topics/git/how_to_install_git/index.md
+++ b/doc/topics/git/how_to_install_git/index.md
@@ -4,6 +4,7 @@ author_gitlab: SeanPackham
level: beginner
article_type: user guide
date: 2017-05-15
+description: 'This article describes how to install Git on macOS, Ubuntu Linux and Windows.'
---
# Installing Git
diff --git a/doc/university/README.md b/doc/university/README.md
index 55865ac23e8..aa68c841f92 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -28,7 +28,6 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 1.1. Version Control and Git
1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
-1. [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
#### 1.2. GitLab Basics
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index a9ccbf5a085..945d6a578b0 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -89,7 +89,7 @@ A [copy](https://git-scm.com/docs/git-clone) of a repository stored on your mach
### Code Review
-Examination of a progam's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab.
+Examination of a program's code. The main aim is to maintain high quality standards of code that is being shipped. Merge requests [serve as a code review tool](https://about.gitlab.com/2014/09/29/gitlab-flow/) in GitLab.
### Code Snippet
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
index 47ccd0e6dbc..f340164b882 100644
--- a/doc/university/high-availability/aws/README.md
+++ b/doc/university/high-availability/aws/README.md
@@ -354,11 +354,11 @@ add the following script to the User Data section:
- mount -a -t nfs
- sudo gitlab-ctl reconfigure
-On the security group section we can chosse our existing
+On the security group section we can choose our existing
`gitlab-ec2-security-group` group which has already been tested.
After this is launched we are able to start creating our Auto Scaling
-Group. Start by giving it a name and assinging it our VPC and private
+Group. Start by giving it a name and assigning it our VPC and private
subnets. We also want to always start with two instances and if you
scroll down to Advanced Details we can choose to receive traffic from ELBs.
Lets enable that option and select our ELB. We also want to use the ELB's
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 25d5fe351ca..d1d5db6bbcd 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -163,7 +163,7 @@ Some tickets need specific knowledge or a deep understanding of a particular com
- Aim to have a good understanding of the problems that customers are facing
- Aim to have gained experience in scheduling and participating in calls with customers
-- Aim to have a good understanding of ticket flow through Zendesk and how to interat with our various channels
+- Aim to have a good understanding of ticket flow through Zendesk and how to interact with our various channels
### Stage 4
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index a882bf0eb48..9b8a8db58e2 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -27,7 +27,7 @@ project.
### Short Story of Git
-- 1991-2002: The Linux kernel was being maintaned by sharing archived files
+- 1991-2002: The Linux kernel was being maintained by sharing archived files
and patches.
- 2002: The Linux kernel project began using a DVCS called BitKeeper
- 2005: BitKeeper revoked the free-of-charge status and Git was created
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
index 02a6ad48a38..d7bc7bda43f 100644
--- a/doc/university/training/gitlab_flow.md
+++ b/doc/university/training/gitlab_flow.md
@@ -2,39 +2,31 @@
comments: false
---
-# GitLab Flow
+# What is the GitLab Flow
- A simplified branching strategy
- All features and fixes first go to master
- Allows for 'production' or 'stable' branches
- Bug fixes/hot fix patches are cherry-picked from master
----
-
-# Feature branches
+## Feature branches
- Create a feature/bugfix branch to do all work
- Use merge requests to merge to master
![inline](gitlab_flow/feature_branches.png)
----
-
-# Production branch
+## Production branch
- One, long-running production release branch
as opposed to individual stable branches
- Consider creating a tag for each version that gets deployed
----
-
-# Production branch
+## Production branch
![inline](gitlab_flow/production_branch.png)
----
-
-# Release branch
+## Release branch
- Useful if you release software to customers
- When preparing a new release, create stable branch
@@ -43,15 +35,11 @@ comments: false
- Cherry-pick critical bug fixes to stable branch for patch release
- Never commit bug fixes directly to stable branch
----
-
-# Release branch
+## Release branch
![inline](gitlab_flow/release_branches.png)
----
-
-# More details
+## More details
-Blog post on 'GitLab Flow' at
-[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
+For more information read through the [GitLab Flow](../../workflow/gitlab_flow.md)
+documentation.
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
index b8049b5c80e..f6006ce0a60 100644
--- a/doc/university/training/topics/gitlab_flow.md
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -1,57 +1,3 @@
---
-comments: false
+redirect_to: '../gitlab_flow.md'
---
-
-# GitLab Flow
-
-----------
-
-- A simplified branching strategy
-- All features and fixes first go to master
-- Allows for 'production' or 'stable' branches
-- Bug fixes/hot fix patches are cherry-picked from master
-
-----------
-
-### Feature branches
-
-- Create a feature/bugfix branch to do all work
-- Use merge requests to merge to master
-
-![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/feature_branches.png)
-
-----------
-
-## Production branch
-
-- One, long-running production release branch
- as opposed to individual stable branches
-- Consider creating a tag for each version that gets deployed
-
-----------
-
-## Production branch
-
-![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/production_branch.png)
-
-----------
-
-## Release branch
-
-- Useful if you release software to customers
-- When preparing a new release, create stable branch
- from master
-- Consider creating a tag for each version
-- Cherry-pick critical bug fixes to stable branch for patch release
-- Never commit bug fixes directly to stable branch
-
-----------
-
-![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/release_branches.png)
-
-----------
-
-## More details
-
-Blog post on 'GitLab Flow' at
-[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
index 4e8c9de85a1..d7b771cd87b 100644
--- a/doc/university/training/topics/merge_requests.md
+++ b/doc/university/training/topics/merge_requests.md
@@ -2,7 +2,7 @@
comments: false
---
-# Merge requests
+# Code review and collaboration with Merge Requests
----------
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index ab48d52d3c3..6333ceedbd7 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -9,7 +9,7 @@ comments: false
- 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 anotated release tag with a stable branch
+- Many projects combine an annotated release tag with a stable branch
- Consider setting deployment/release tags automatically
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 90e1d2ba5e8..dccb6cbf071 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -279,7 +279,7 @@ See GitLab merge requests for examples:
- 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 anotated release tag with a stable branch
+- Many projects combine an annotated release tag with a stable branch
- Consider setting deployment/release tags automatically
---
diff --git a/doc/update/10.7-to-10.8.md b/doc/update/10.7-to-10.8.md
new file mode 100644
index 00000000000..13101a987f4
--- /dev/null
+++ b/doc/update/10.7-to-10.8.md
@@ -0,0 +1,362 @@
+---
+comments: false
+---
+
+# From 10.7 to 10.8
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag 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 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.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.3/ruby-2.3.7.tar.gz
+ echo '540996fec64984ab6099e34d2f5820b14904f15a ruby-2.3.7.tar.gz' | shasum -c - && tar xzf ruby-2.3.7.tar.gz
+ cd ruby-2.3.7
+
+ ./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 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.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://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.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 10-8-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 10-8-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
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+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 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
+```
+
+### 11. Update configuration files
+
+#### 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/10-7-stable:config/gitlab.yml.example origin/10-8-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/10-7-stable:lib/support/nginx/gitlab-ssl origin/10-8-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/10-7-stable:lib/support/nginx/gitlab origin/10-8-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/10-8-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/10-8-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/10-7-stable:lib/support/init.d/gitlab.default.example origin/10-8-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
+```
+
+### 12. 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).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. 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 (10.7)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.6 to 10.7](10.6-to-10.7.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/10-8-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-8-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/admin_area/labels.md b/doc/user/admin_area/labels.md
index 9e2a89ebdf6..e383142c33e 100644
--- a/doc/user/admin_area/labels.md
+++ b/doc/user/admin_area/labels.md
@@ -1,4 +1,4 @@
-# Labels
+# Labels administration **[CORE ONLY]**
## Default Labels
diff --git a/doc/user/admin_area/settings/img/enforce_terms.png b/doc/user/admin_area/settings/img/enforce_terms.png
new file mode 100644
index 00000000000..e5f0a2683b5
--- /dev/null
+++ b/doc/user/admin_area/settings/img/enforce_terms.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/respond_to_terms.png b/doc/user/admin_area/settings/img/respond_to_terms.png
new file mode 100644
index 00000000000..d0d086c3498
--- /dev/null
+++ b/doc/user/admin_area/settings/img/respond_to_terms.png
Binary files differ
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index 603b826e7f2..26329f20339 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -1,7 +1,7 @@
# Sign-up restrictions
You can block email addresses of specific domains, or whitelist only some
-specifc domains via the **Application Settings** in the Admin area.
+specific domains via the **Application Settings** in the Admin area.
>**Note**: These restrictions are only applied during sign-up. An admin is
able to add add a user through the admin panel with a disallowed domain. Also
diff --git a/doc/user/admin_area/settings/terms.md b/doc/user/admin_area/settings/terms.md
new file mode 100644
index 00000000000..8e1fb982aba
--- /dev/null
+++ b/doc/user/admin_area/settings/terms.md
@@ -0,0 +1,38 @@
+# Enforce accepting Terms of Service
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18570)
+> in [GitLab Core](https://about.gitlab.com/pricing/) 10.8
+
+## Configuration
+
+When it is required for all users of the GitLab instance to accept the
+Terms of Service, this can be configured by an admin on the settings
+page:
+
+![Enable enforcing Terms of Service](img/enforce_terms.png).
+
+The terms itself can be entered using Markdown. For each update to the
+terms, a new version is stored. When a user accepts or declines the
+terms, GitLab will keep track of which version they accepted or
+declined.
+
+When an admin enables this feature, they will automattically be
+directed to the page to accept the terms themselves. After they
+accept, they will be directed back to the settings page.
+
+## Accepting terms
+
+When this feature was enabled, the users that have not accepted the
+terms of service will be presented with a screen where they can either
+accept or decline the terms.
+
+![Respond to terms](img/respond_to_terms.png)
+
+When the user accepts the terms, they will be directed to where they
+were going. After a sign-in or sign-up this will most likely be the
+dashboard.
+
+When the user was already logged in when the feature was turned on,
+they will be asked to accept the terms on their next interaction.
+
+When a user declines the terms, they will be signed out.
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 7baccb796c6..0c1cd113686 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -75,7 +75,6 @@ Shared Runners on GitLab.com run in [autoscale mode] and powered by
Google Cloud Platform and DigitalOcean. Autoscaling means reduced
waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security.
-
They're free to use for public open source projects and limited to 2000 CI
minutes per month per group for private projects. Read about all
[GitLab.com plans](https://about.gitlab.com/pricing/).
@@ -90,6 +89,10 @@ ephemeral instances with 3.75GB of RAM, CoreOS and the latest Docker Engine
installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default
region of the VMs is US East1.
+Jobs handled by the shared Runners on GitLab.com (`shared-runners-manager-X.gitlab.com`),
+**will be timed out after 3 hours**, regardless of the timeout configured in a
+project. Check the issues [4010] and [4070] for the reference.
+
Below are the shared Runners settings.
| Setting | GitLab.com | Default |
@@ -340,3 +343,5 @@ High Performance TCP/HTTP Load Balancer:
[mailgun]: https://www.mailgun.com/ "Mailgun website"
[sidekiq]: http://sidekiq.org/ "Sidekiq website"
[unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer"
+[4010]: https://gitlab.com/gitlab-com/infrastructure/issues/4010 "Find a good value for maximum timeout for Shared Runners"
+[4070]: https://gitlab.com/gitlab-com/infrastructure/issues/4070 "Configure per-runner timeout for shared-runners-manager-X on GitLab.com"
diff --git a/doc/user/group/img/groups.png b/doc/user/group/img/groups.png
index 6211f999d5e..3173ddce7ff 100644
--- a/doc/user/group/img/groups.png
+++ b/doc/user/group/img/groups.png
Binary files differ
diff --git a/doc/user/group/img/new_group_from_groups.png b/doc/user/group/img/new_group_from_groups.png
index baf34244cb2..9c5dd7ebd8b 100644
--- a/doc/user/group/img/new_group_from_groups.png
+++ b/doc/user/group/img/new_group_from_groups.png
Binary files differ
diff --git a/doc/user/group/img/new_group_from_other_pages.png b/doc/user/group/img/new_group_from_other_pages.png
index 014a7088af2..77427224447 100644
--- a/doc/user/group/img/new_group_from_other_pages.png
+++ b/doc/user/group/img/new_group_from_other_pages.png
Binary files differ
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 88f4bb2ee04..30761a66563 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -40,20 +40,20 @@ In GitLab, a namespace is a unique name to be used as a user name, a group name,
- `http://gitlab.example.com/groupname`
- `http://gitlab.example.com/groupname/subgroup_name`
-For example, consider a user called John:
+For example, consider a user named Alex:
-1. John creates his account on GitLab.com with the username `john`;
-his profile will be accessed under `https://gitlab.example.com/john`
-1. John creates a group for his team with the groupname `john-team`;
-his group and its projects will be accessed under `https://gitlab.example.com/john-team`
-1. John creates a subgroup of `john-team` with the subgroup name `marketing`;
-his subgroup and its projects will be accessed under `https://gitlab.example.com/john-team/marketing`
+1. Alex creates an account on GitLab.com with the username `alex`;
+their profile will be accessed under `https://gitlab.example.com/alex`
+1. Alex creates a group for their team with the groupname `alex-team`;
+the group and its projects will be accessed under `https://gitlab.example.com/alex-team`
+1. Alex creates a subgroup of `alex-team` with the subgroup name `marketing`;
+this subgroup and its projects will be accessed under `https://gitlab.example.com/alex-team/marketing`
By doing so:
-- Any team member mentions John with `@john`
-- John mentions everyone from his team with `@john-team`
-- John mentions only his marketing team with `@john-team/marketing`
+- Any team member mentions Alex with `@alex`
+- Alex mentions everyone from their team with `@alex-team`
+- Alex mentions only the marketing team with `@alex-team/marketing`
## Issues and merge requests within a group
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 2a982344e5f..02f8ef08117 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -55,7 +55,7 @@ first group being the name of the distro and subsequent groups split like:
Another example of GitLab as a company would be the following:
- Organization Group - GitLab
- - Category Subroup - Marketing
+ - Category Subgroup - Marketing
- (project) Design
- (project) General
- Category Subgroup - Software
diff --git a/doc/user/index.md b/doc/user/index.md
index 43b6fd53b91..a50e5e8fbf8 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -1,3 +1,7 @@
+---
+description: 'Read through the GitLab User documentation to learn how to use, configure, and customize GitLab and GitLab.com to your own needs.'
+---
+
# User documentation
Welcome to GitLab! We're glad to have you here!
@@ -56,7 +60,7 @@ With GitLab Enterprise Edition, you can also:
[Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#merge-request-approvals),
[Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
and [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards)
-- Create formal relashionships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html)
+- Create formal relationships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html)
- Use [Burndown Charts](https://docs.gitlab.com/ee/user/project/milestones/burndown_charts.html) to track progress during a sprint or while working on a new version of their software.
- Leverage [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html) with [Advanced Global Search](https://docs.gitlab.com/ee/user/search/advanced_global_search.html) and [Advanced Syntax Search](https://docs.gitlab.com/ee/user/search/advanced_search_syntax.html) for faster, more advanced code search across your entire GitLab instance
- [Authenticate users with Kerberos](https://docs.gitlab.com/ee/integration/kerberos.html)
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index a9ba2a51242..0808e3949be 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -1,3 +1,7 @@
+---
+description: 'Understand and explore the user permission levels in GitLab, and what features each of them grants you access to.'
+---
+
# Permissions
Users have different abilities depending on the access level they have in a
diff --git a/doc/user/profile/active_sessions.md b/doc/user/profile/active_sessions.md
new file mode 100644
index 00000000000..5119c0e30d0
--- /dev/null
+++ b/doc/user/profile/active_sessions.md
@@ -0,0 +1,20 @@
+# Active Sessions
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17867)
+> in GitLab 10.8.
+
+GitLab lists all devices that have logged into your account. This allows you to
+review the sessions and revoke any of it that you don't recognize.
+
+## Listing all active sessions
+
+1. On the upper right corner, click on your avatar and go to your **Settings**.
+1. Navigate to the **Active Sessions** tab.
+
+![Active sessions list](img/active_sessions_list.png)
+
+## Revoking a session
+
+1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**.
+1. Click on **Revoke** besides a session. The current session cannot be
+ revoked, as this would sign you out of GitLab.
diff --git a/doc/user/profile/img/active_sessions_list.png b/doc/user/profile/img/active_sessions_list.png
new file mode 100644
index 00000000000..76a52220bcd
--- /dev/null
+++ b/doc/user/profile/img/active_sessions_list.png
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index ab16f8d14c1..91cdef8d1dd 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -39,6 +39,7 @@ From there, you can:
- Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH
- Manage your [preferences](preferences.md#syntax-highlighting-theme)
to customize your own GitLab experience
+- [View your active sessions](active_sessions.md) and revoke any of them if necessary
- Access your audit log, a security log of important events involving your account
## Changing your username
diff --git a/doc/user/project/bulk_editing.md b/doc/user/project/bulk_editing.md
new file mode 100644
index 00000000000..324a7fa6603
--- /dev/null
+++ b/doc/user/project/bulk_editing.md
@@ -0,0 +1,17 @@
+# Bulk Editing
+
+>**Note:**
+- A permission level of `Reporter` or higher is required in order to manage
+issues.
+- A permission level of `Developer` or higher is required in order to manage
+merge requests.
+
+Fields across multiple issues or merge requests can be updated simutaneously by using the bulk edit feature.
+
+>**Note:**
+- Bulk editing of issues and merge requests is only available at the project level.
+
+To access the feature, navigate to either the issue or merge request list for the project and click 'Edit Issues' or 'Edit Merge Requests'. This will cause a sidebar to be shown on the right-hand side of the screen, where the available, editable fields are displayed. Checkboxes will also appear to the left-hand side of each issue or merge request, ready to be selected.
+
+Once all items have been selected, choose the appropriate fields and their values from the sidebar and click 'Update All' to apply these changes.
+
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 716787532fc..edb875bc7e6 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -238,6 +238,7 @@ work.
The default environment scope is `*`, which means all jobs, regardless of their
environment, will use that cluster. Each scope can only be used by a single
cluster in a project, and a validation error will occur if otherwise.
+Also, jobs that don't have an environment keyword set will not be able to access any cluster.
---
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index 86fc58020e8..c09d5aeba8e 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje
![Personal access tokens page](img/deploy_tokens.png)
-## Revoking a personal access token
+## Revoking a deploy token
At any time, you can revoke any deploy token by just clicking the
respective **Revoke** button under the 'Active deploy tokens' area.
@@ -71,6 +71,16 @@ docker login registry.example.com -u <username> -p <deploy_token>
Just replace `<username>` and `<deploy_token>` with the proper values. Then you can simply
pull images from your Container Registry.
+### GitLab Deploy Token
+
+> [Introduced][ce-18414] in GitLab 10.8.
+
+There's a special case when it comes to Deploy Tokens, if a user creates one
+named `gitlab-deploy-token`, the username and token of the Deploy Token will be
+automatically exposed to the CI/CD jobs as environment variables: `CI_DEPLOY_USER` and
+`CI_DEPLOY_PASSWORD`, respectively.
+
[ce-17894]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894
[ce-11845]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845
+[ce-18414]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18414
[container registry]: ../container_registry.md
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 5933bcedc8b..4d5b2c97291 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -111,7 +111,7 @@ in the table below.
| ----- | ----------- |
| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
-| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
+| `Username` | The user name created in [configuring JIRA step](#configuring-jira). Using the email address will cause `401 unauthorized`. |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
| `Transition ID` | This is the ID of a transition that moves issues to the desired state. **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 9496d6f2ce0..074eeb729e3 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -34,7 +34,7 @@ Click on the service links to see further configuration instructions and details
| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
-| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
+| Gemnasium _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index fea3231006b..557487e1a75 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -10,17 +10,19 @@ The [Prometheus service](../prometheus.md) must be enabled.
## Metrics supported
+NGINX server metrics are detected, which tracks the pages and content directly served by NGINX.
+
| Name | Query |
| ---- | ----- |
-| Throughput (req/sec) | sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code) |
-| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) |
-| HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) |
+| Throughput (req/sec) | sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code) |
+| Latency (ms) | avg(nginx_server_requestMsec{%{environment_filter}}) |
+| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m])) |
## Configuring Prometheus to monitor for NGINX metrics
To get started with NGINX monitoring, you should first enable the [VTS statistics](https://github.com/vozlt/nginx-module-vts)) module for your NGINX server. This will capture and display statistics in an HTML readable form. Next, you should install and configure the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter) which parses these statistics and translates them into a Prometheus monitoring endpoint.
-If you are using NGINX as your Kubernetes ingress, there is [upcoming direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release.
+If you are using NGINX as your Kubernetes ingress, GitLab will [automatically detect](nginx_ingress.md) the metrics once enabled in 0.9.0 and later releases.
## Specifying the Environment label
diff --git a/doc/user/project/issues/closing_issues.md b/doc/user/project/issues/closing_issues.md
index dcfa5ff59b2..1d88745af9f 100644
--- a/doc/user/project/issues/closing_issues.md
+++ b/doc/user/project/issues/closing_issues.md
@@ -48,12 +48,12 @@ link to each other, but the MR will NOT close the issue(s) when merged.
## From the Issue Board
-You can close an issue from [Issue Boards](../issue_board.md) by draging an issue card
+You can close an issue from [Issue Boards](../issue_board.md) by dragging an issue card
from its list and dropping into **Closed**.
![close issue from the Issue Board](img/close_issue_from_board.gif)
-## Customizing the issue closing patern
+## Customizing the issue closing pattern
Alternatively, a GitLab **administrator** can
-[customize the issue closing patern](../../../administration/issue_closing_pattern.md).
+[customize the issue closing pattern](../../../administration/issue_closing_pattern.md).
diff --git a/doc/user/project/issues/crosslinking_issues.md b/doc/user/project/issues/crosslinking_issues.md
index cc8988be36b..786d1c81b1b 100644
--- a/doc/user/project/issues/crosslinking_issues.md
+++ b/doc/user/project/issues/crosslinking_issues.md
@@ -60,4 +60,4 @@ or simply link both issue and merge request as described in the
### Close an issue by merging a merge request
-To [close an issue when a merge request is merged](closing_issues.md#via-merge-request), use the [automatic issue closing patern](automatic_issue_closing.md).
+To [close an issue when a merge request is merged](closing_issues.md#via-merge-request), use the [automatic issue closing pattern](automatic_issue_closing.md).
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index be4436749f9..bf17731c523 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -151,3 +151,7 @@ or Bugzilla.
### Issue's API
Read through the [API documentation](../../../api/issues.md).
+
+### Bulk editing issues
+
+Find out about [bulk editing issues](../../project/bulk_editing.md).
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index 6bcf7686a71..e9903b01c82 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -28,7 +28,7 @@ Comments and system notes also appear automatically in response to various actio
#### 2. Todos
- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list
-- Mark done: mark that issue as done (reflects on the Todo list)
+- Mark todo as done: mark that issue as done (reflects on the Todo list)
#### 3. Assignee
@@ -152,7 +152,7 @@ know you like it without spamming them.
These text fields also fully support
[GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
-#### 17. Comment, start a discusion, or comment and close
+#### 17. Comment, start a discussion, or comment and close
Once you wrote your comment, you can either:
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index a6c0fd49c45..5932f5a2bc1 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -233,6 +233,10 @@ all your changes will be available to preview by anyone with the Review Apps lin
[Read more about Review Apps.](../../../ci/review_apps/index.md)
+## Bulk editing merge requests
+
+Find out about [bulk editing merge requests](../../project/bulk_editing.md).
+
## Tips
Here are some tips that will help you be more efficient with merge requests in
diff --git a/doc/user/project/merge_requests/maintainer_access.md b/doc/user/project/merge_requests/maintainer_access.md
index c9763a3fe02..89f71e16a50 100644
--- a/doc/user/project/merge_requests/maintainer_access.md
+++ b/doc/user/project/merge_requests/maintainer_access.md
@@ -16,3 +16,5 @@ source project, and only lasts while the merge request is open.
Enable this functionality while creating a merge request:
![Enable maintainer edits](./img/allow_maintainer_push.png)
+
+[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index 68c49054e47..ecbc8534eea 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -29,7 +29,7 @@ The merge conflict resolution editor allows for more complex merge conflicts,
which require the user to manually modify a file in order to resolve a conflict,
to be solved right form the GitLab interface. Use the **Edit inline** button
to open the editor. Once you're sure about your changes, hit the
-**Commit conflict resolution** button.
+**Commit to source branch** button.
![Merge conflict editor](img/merge_conflict_editor.png)
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 10e6321eb82..64bb33be547 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -10,7 +10,7 @@ Milestones allow you to organize issues and merge requests into a cohesive group
- **Project milestones** can be assigned to issues or merge requests in that project only.
- **Group milestones** can be assigned to any issue or merge request of any project in that group.
-- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/36862), you will be able to assign group milestones to issues and merge reqeusts of projects in [subgroups](../../group/subgroups/index.md).
+- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/36862), you will be able to assign group milestones to issues and merge requests of projects in [subgroups](../../group/subgroups/index.md).
## Creating milestones
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 430fe3af1f8..61af1d2ab27 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -70,7 +70,7 @@ In case you want to point a root domain (`example.com`) to your
GitLab Pages site, deployed to `namespace.gitlab.io`, you need to
log into your domain's admin control panel and add a DNS `A` record
pointing your domain to Pages' server IP address. For projects on
-GitLab.com, this IP is `52.167.214.135`. For projects leaving in
+GitLab.com, this IP is `52.167.214.135`. For projects living in
other GitLab instances (CE or EE), please contact your sysadmin
asking for this information (which IP address is Pages server
running on your instance).
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index 2274cac8ace..556bf1db116 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -50,14 +50,14 @@ created for the steps below.
1. [Fork a sample project](../../../gitlab-basics/fork-project.md) from the [Pages group](https://gitlab.com/pages)
1. Trigger a build (push a change to any file)
1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your project's **Settings** > **Pages**
-1. Optionally, remove the fork relationship by navigating to your project's **Settings** > expanding **Advanced settings** and scrolling down to **Remove fork relashionship**:
+1. Optionally, remove the fork relationship by navigating to your project's **Settings** > expanding **Advanced settings** and scrolling down to **Remove fork relationship**:
- ![remove fork relashionship](img/remove_fork_relashionship.png)
+ ![remove fork relationship](img/remove_fork_relationship.png)
To turn a **project website** forked from the Pages group into a **user/group** website, you'll need to:
- Rename it to `namespace.gitlab.io`: navigate to project's **Settings** > expand **Advanced settings** > and scroll down to **Rename repository**
-- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likelly, it will be in the SSG's config file.
+- Adjust your SSG's [base URL](#urls-and-baseurls) to from `"project-name"` to `""`. This setting will be at a different place for each SSG, as each of them have their own structure and file tree. Most likely, it will be in the SSG's config file.
> **Notes:**
>
diff --git a/doc/user/project/pages/img/remove_fork_relashionship.png b/doc/user/project/pages/img/remove_fork_relationship.png
index 67c45491f08..67c45491f08 100644
--- a/doc/user/project/pages/img/remove_fork_relashionship.png
+++ b/doc/user/project/pages/img/remove_fork_relationship.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index a65aa758198..4b5c2539c4b 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,23 +1,26 @@
-# GitLab Pages
+---
+description: 'Learn how to use GitLab Pages to deploy a static website at no additional cost.'
+---
-With GitLab Pages you can host your website at no cost.
+# GitLab Pages
-Your files live in a GitLab project's [repository](../repository/index.md),
-from which you can deploy [static websites](#explore-gitlab-pages).
-GitLab Pages supports all static site generators (SSGs).
+With GitLab Pages it's easy to publish your project website. GitLab Pages is a hosting service for static websites, at no additional cost.
## Getting Started
-Follow the steps below to get your website live. They shouldn't take more than
-5 minutes to complete:
+[Create a project from scratch](getting_started_part_two.md#create-a-project-from-scratch)
+to get you started quickly, or,
+alternatively, start from an existing project as follows:
-- 1. [Fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project) an [example project](https://gitlab.com/pages)
-- 2. Change a file to trigger a GitLab CI/CD pipeline
-- 3. Visit your project's **Settings > Pages** to see your **website link**, and click on it. Bam! Your website is live.
+- 1. [Fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project) an [example project](https://gitlab.com/pages):
+by forking a project, you create a copy of the codebase you're forking from to start from a template instead of starting from scratch.
+- 2. Change a file to trigger a GitLab CI/CD pipeline: GitLab CI/CD will build and deploy your site to GitLab Pages.
+- 3. Visit your project's **Settings > Pages** to see your **website link**, and click on it. Bam! Your website is live! :)
_Further steps (optional):_
-- 4. Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from) (_You don't need the relationship unless you intent to contribute back to the example project you forked from_).
+- 4. Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from)
+(_You don't need the relationship unless you intent to contribute back to the example project you forked from_).
- 5. Make it a [user/group website](getting_started_part_one.md#user-and-group-websites)
**Watch a video with the steps above: https://www.youtube.com/watch?v=TWqh9MtT4Bg**
@@ -27,14 +30,23 @@ _Advanced options:_
- [Use a custom domain](getting_started_part_three.md#adding-your-custom-domain-to-gitlab-pages)
- Apply [SSL/TLS certification](getting_started_part_three.md#ssl-tls-certificates) to your custom domain
-## Explore GitLab Pages
+## How Does It Work?
With GitLab Pages you can create [static websites](getting_started_part_one.md#what-you-need-to-know-before-getting-started)
-for your GitLab projects, groups, or user accounts. You can use any static
-website generator: Jekyll, Middleman, Hexo, Hugo, Pelican, you name it!
+for your GitLab projects, groups, or user accounts.
+
+It supports plain static content, such as HTML, and **all** [static site generators (SSGs)](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/), such as Jekyll, Middleman, Hexo, Hugo, and Pelican.
+
Connect as many custom domains as you like and bring your own TLS certificate
to secure them.
+Your files live in a project [repository](../repository/index.md) on GitLab.
+[GitLab CI](../../../ci/README.md) picks up those files and makes them available at, typically,
+`http://<username>.gilab.io/<projectname>`. Please read through the docs on
+[GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain) for more info.
+
+## Explore GitLab Pages
+
Read the following tutorials to know more about:
- [Static websites and GitLab Pages domains](getting_started_part_one.md): Understand what is a static website, and how GitLab Pages default domains work
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 0b5076b8c5d..fe4d15adfa1 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -1,4 +1,4 @@
-# GitLab Pages
+# Exploring GitLab Pages
> **Notes:**
> - This feature was [introduced][ee-80] in GitLab EE 8.3.
@@ -14,9 +14,7 @@ deploy static pages for your individual projects, your user or your group.
Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
information, if you are using GitLab.com to host your website.
-Read through [All you Need to Know About GitLab Pages][pages-index-guide] for a list of all learning materials we have prepared for GitLab Pages (webpages, articles, guides, blog posts, video tutorials).
-
-## Getting started with GitLab Pages
+## Getting started with GitLab Pages domains
> **Note:**
> In the rest of this document we will assume that the general domain name that
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 442fc978284..2f4ed3493c2 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -38,6 +38,7 @@ do.
| `/award :emoji:` | Toggle award for :emoji: |
| `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
-| `/move path/to/project` | Moves issue to another project |
-| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
-| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | \ No newline at end of file
+| `/move path/to/project` | Moves issue to another project |
+| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
+| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
+| <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request |
diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md
index 08805a4dc99..a06ecc3220f 100644
--- a/doc/user/project/repository/reducing_the_repo_size_using_git.md
+++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md
@@ -1,6 +1,6 @@
# Reducing the repository size using Git
-A GitLab Entrerprise Edition administrator can set a [repository size limit][admin-repo-size]
+A GitLab Enterprise Edition administrator can set a [repository size limit][admin-repo-size]
which will prevent you to exceed it.
When a project has reached its size limit, you will not be able to push to it,
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index eb0ac221e30..b8bac01959e 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -5,6 +5,7 @@
> - [Introduced][ce-3050] in GitLab 8.9.
> - Importing will not be possible if the import instance version differs from
> that of the exporter.
+> - For GitLab admins, please read through [Project import/export administration](../../../administration/raketasks/project_import_export.md).
> - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'.
> Ask your administrator if you don't see the **GitLab export** button when
@@ -31,7 +32,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.4 to current | 0.2.2 |
+| 10.8 to current | 0.2.3 |
+| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
diff --git a/doc/user/project/web_ide/img/commit_changes.png b/doc/user/project/web_ide/img/commit_changes.png
index b6fcbf699aa..a5364c12760 100644
--- a/doc/user/project/web_ide/img/commit_changes.png
+++ b/doc/user/project/web_ide/img/commit_changes.png
Binary files differ
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index b7064b83c4e..105d8a6ab61 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -13,15 +13,27 @@ and from merge requests.
![Open Web IDE](img/open_web_ide.png)
-## Commit changes
+## File finder
-Changed files are shown on the right in the commit panel. All changes are
-automatically staged. To commit your changes, add a commit message and click
-the 'Commit Button'.
+> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18323) [GitLab Core][ce] 10.8.
+
+The file finder allows you to quickly open files in the current branch by
+searching. The file finder is launched using the keyboard shortcut `Command-p`,
+`Control-p`, or `t` (when editor is not in focus). Type the filename or
+file path fragments to start seeing results.
+
+## Stage and commit changes
+
+After making your changes, click the Commit button in the bottom left to
+review the list of changed files. Click on each file to review the changes and
+click the tick icon to stage the file.
+
+Once you have staged some changes, you can add a commit message and commit the
+staged changes. Unstaged changes will not be commited.
![Commit changes](img/commit_changes.png)
-## Comparing changes
+## Reviewing changes
Before you commit your changes, you can compare them with the previous commit
by switching to the review mode or selecting the file from the staged files
@@ -30,4 +42,5 @@ list.
An additional review mode is available when you open a merge request, which
shows you a preview of the merge request diff if you commit your changes.
+[ce]: https://about.gitlab.com/pricing/
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 2b23c494dc4..4f1b96b775c 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -96,7 +96,7 @@ On the field **Filter by name**, type the project or group name you want to find
will filter them for you as you type.
You can also look for the projects you starred (**Starred projects**), and **Explore** all
-public and internal projects available in GitLab.com, from which you can filter by visibitily,
+public and internal projects available in GitLab.com, from which you can filter by visibility,
through **Trending**, best rated with **Most starts**, or **All** of them.
You can also sort them by **Name**, **Last created**, **Oldest created**, **Last updated**,
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index 2170b079f62..8397c0b00ef 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -27,3 +27,36 @@ Personal snippets are not related to any project and can be created completely i
You can download the raw content of a snippet.
By default snippets will be downloaded with Linux-style line endings (`LF`). If you want to preserve the original line endings you need to add a parameter `line_ending=raw` (eg. `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
+
+## Embedded Snippets
+
+> Introduced in GitLab 10.8.
+
+Public snippets can not only be shared, but also embedded on any website. This
+allows to reuse a GitLab snippet in multiple places and any change to the source
+is automatically reflected in the embedded snippet.
+
+To embed a snippet, first make sure that:
+
+- The project is public (if it's a project snippet)
+- The snippet is public
+- In **Project > Settings > Permissions**, the snippets permissions are
+ set to **Everyone with access**
+
+Once the above conditions are met, the "Embed" section will appear in your snippet
+where you can simply click on the "Copy to clipboard" button. This copies a one-line
+script that you can add to any website or blog post.
+
+Here's how an example code looks like:
+
+```html
+<script src="https://gitlab.com/namespace/project/snippets/SNIPPET_ID.js"></script>
+```
+
+Here's how an embedded snippet looks like:
+
+<script src="https://gitlab.com/gitlab-org/gitlab-ce/snippets/1717978.js"></script>
+
+Embedded snippets are displayed with a header that shows the file name if defined,
+the snippet size, a link to GitLab, and the actual snippet content. Actions in
+the header allow users to see the snippet in raw format and download it.
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 377eee69c11..0d592a6d43e 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -243,4 +243,12 @@ GitLab checks files to detect LFS pointers on push. If LFS pointers are detected
Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
-If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
+If you are storing LFS files outside of GitLab you can disable LFS on the project by setting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
+
+### Hosting LFS objects externally
+
+It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
+
+Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
+
+LFS can be disabled from the [Project settings](../../user/project/settings/index.md).
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index a68bb8b27ca..dc6da1938f3 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -1 +1 @@
-This document was moved to [user/project/merge_requests](../user/project/merge_requests.md).
+This document was moved to [user/project/merge_requests/index.md](../user/project/merge_requests/index.md).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index f1501c81b27..fc592b99860 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -106,6 +106,10 @@ by yourself (except when an issue is due). You will only receive automatic
notifications when somebody else comments or adds changes to the ones that
you've created or mentions you.
+If a merge request becomes unmergeable, its author will be notified about the cause.
+If a user has also set the merge request to automatically merge once pipeline succeeds,
+then that user will also be notified.
+
### Email Headers
Notification emails include headers that provide extra content about the notification received:
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index aa48b8f750e..ced7d391ace 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -1 +1 @@
-This document is moved to [user/project/protected_branches.md](../user/project/protected_branches.md)
+This document was moved to [another location](../user/project/protected_branches.md).
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
new file mode 100644
index 00000000000..dbe63144e38
--- /dev/null
+++ b/doc/workflow/repository_mirroring.md
@@ -0,0 +1,111 @@
+# Repository mirroring
+
+Repository Mirroring is a way to mirror repositories from external sources.
+It can be used to mirror all branches, tags, and commits that you have
+in your repository.
+
+Your mirror at GitLab will be updated automatically. You can
+also manually trigger an update at most once every 5 minutes.
+
+## Overview
+
+Repository mirroring is very useful when, for some reason, you must use a
+project from another source.
+
+There are two kinds of repository mirroring features supported by GitLab:
+**push** and **pull**, the latter being only available in GitLab Enterprise Edition.
+The **push** method mirrors the repository in GitLab to another location.
+
+Once the mirror repository is updated, all new branches,
+tags, and commits will be visible in the project's activity feed.
+Users with at least [developer access][perms] to the project can also force an
+immediate update with the click of a button. This button will not be available if
+the mirror is already being updated or 5 minutes still haven't passed since its last update.
+
+A few things/limitations to consider:
+
+- The repository must be accessible over `http://`, `https://`, `ssh://` or `git://`.
+- If your HTTP repository is not publicly accessible, add authentication
+ information to the URL, like: `https://username@gitlab.company.com/group/project.git`.
+ In some cases, you might need to use a personal access token instead of a
+ password, e.g., you want to mirror to GitHub and have 2FA enabled.
+- The import will time out after 15 minutes. For repositories that take longer
+ use a clone/push combination.
+- The Git LFS objects will not be synced. You'll need to push/pull them
+ manually.
+
+## Use-case
+
+- You have old projects in another source that you don't use actively anymore,
+ but don't want to remove for archiving purposes. In that case, you can create
+ a push mirror so that your active GitLab repository can push its changes to the
+ old location.
+
+## Pushing to a remote repository **[STARTER]**
+
+>[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in
+GitLab Enterprise Edition 8.7. [Moved to GitLab Community Edition][ce-18715] in 10.8.
+
+For an existing project, you can set up push mirror from your project's
+**Settings ➔ Repository** and searching for the "Push to a remote repository"
+section. Check the "Remote mirror repository" box and fill in the Git URL of
+the repository to push to. Click **Save changes** for the changes to take
+effect.
+
+![Push settings](repository_mirroring/repository_mirroring_push_settings.png)
+
+When push mirroring is enabled, you are advised not to push commits directly
+to the mirrored repository to prevent the mirror diverging.
+All changes will end up in the mirrored repository whenever commits
+are pushed to GitLab, or when a [forced update](#forcing-an-update) is
+initiated.
+
+Pushes into GitLab are automatically pushed to the remote mirror at least once
+every 5 minutes after they are received or once every minute if **push only
+protected branches** is enabled.
+
+In case of a diverged branch, you will see an error indicated at the **Mirror
+repository** settings.
+
+![Diverged branch](
+repository_mirroring/repository_mirroring_diverged_branch_push.png)
+
+### Push only protected branches
+
+>[Introduced][ee-3350] in GitLab Enterprise Edition 10.3. [Moved to GitLab Community Edition][ce-18715] in 10.8.
+
+You can choose to only push your protected branches from GitLab to your remote repository.
+
+To use this option go to your project's repository settings page under push mirror.
+
+## Setting up a push mirror from GitLab to GitHub
+
+To set up a mirror from GitLab to GitHub, you need to follow these steps:
+
+1. Create a [GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) with the "public_repo" box checked:
+
+ ![edit personal access token GitHub](repository_mirroring/repository_mirroring_github_edit_personal_access_token.png)
+
+1. Fill in the "Git repository URL" with the personal access token replacing the password `https://GitHubUsername:GitHubPersonalAccessToken@github.com/group/project.git`:
+
+ ![push to remote repo](repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png)
+
+1. Save
+1. And either wait or trigger the "Update Now" button:
+
+ ![update now](repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png)
+
+## Forcing an update
+
+While mirrors are scheduled to update automatically, you can always force an update
+by using the **Update now** button which is exposed in various places:
+
+- in the commits page
+- in the branches page
+- in the tags page
+- in the **Mirror repository** settings page
+
+[ee-3350]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350
+[ce-18715]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715
+[perms]: ../user/permissions.md
+
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png
new file mode 100644
index 00000000000..038b05cb31d
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch_push.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png b/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png
new file mode 100644
index 00000000000..139de42d8db
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_github_edit_personal_access_token.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png
new file mode 100644
index 00000000000..ccbc1d92329
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png
new file mode 100644
index 00000000000..b16b3d2828e
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png
Binary files differ
diff --git a/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png b/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png
new file mode 100644
index 00000000000..f8199aa7c0f
--- /dev/null
+++ b/doc/workflow/repository_mirroring/repository_mirroring_push_settings.png
Binary files differ
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 2e1bd6bfe5c..b2f1cbec204 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -11,7 +11,7 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>f</kbd> | Focus filter |
| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar |
| <kbd>?</kbd> | Show/hide this dialog |
-| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
+| <kbd>Cmd</kbd>/<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
## Project Files Browsing
@@ -46,15 +46,19 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
-| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed |
+| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project's activity feed |
| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
-| <kbd>g</kbd> + <kbd>b</kbd> | Go to jobs |
+| <kbd>g</kbd> + <kbd>j</kbd> | Go to jobs |
| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
-| <kbd>g</kbd> + <kbd>g</kbd> | Go to repository charts |
+| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts |
| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to issue boards |
| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to environments |
+| <kbd>g</kbd> + <kbd>k</kbd> | Go to kubernetes |
| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
+| <kbd>g</kbd> + <kbd>w</kbd> | Go to wiki |
| <kbd>t</kbd> | Go to finding file |
| <kbd>i</kbd> | New issue |
@@ -66,8 +70,8 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right |
| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up |
| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down |
-| <kbd>shift</kbd> + <kbd>↑</kbd> or <kbd>shift</kbd> + <kbd>k</kbd> | Scroll to top |
-| <kbd>shift</kbd> + <kbd>↓</kbd> or <kbd>shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
+| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top |
+| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
## Issues and Merge Requests
@@ -84,3 +88,9 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| Keyboard Shortcut | Description |
| ----------------- | ----------- |
| <kbd>e</kbd> | Edit wiki page|
+
+## Web IDE
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>⌘</kbd> + <kbd>p</kbd> | Go to file |
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index e612646cfbc..762bf616268 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -31,6 +31,9 @@ A Todo appears in your Todos dashboard when:
- you are `@mentioned` in a comment on a commit,
- a job in the CI pipeline running for your merge request failed, but this
job is not allowed to fail.
+- a merge request becomes unmergeable, and you are either:
+ - the author, or
+ - have set it to automatically merge once pipeline succeeds.
### Directly addressed Todos
@@ -92,9 +95,9 @@ corresponding **Done** button, and it will disappear from your Todo list.
![A Todo in the Todos dashboard](img/todo_list_item.png)
A Todo can also be marked as done from the issue or merge request sidebar using
-the "Mark done" button.
+the "Mark todo as done" button.
-![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png)
+![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
You can mark all your Todos as done at once by clicking on the **Mark all as
done** button.
diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature
deleted file mode 100644
index 5abc24949cf..00000000000
--- a/features/project/builds/artifacts.feature
+++ /dev/null
@@ -1,65 +0,0 @@
-Feature: Project Builds Artifacts
- Background:
- Given I sign in as a user
- And I own a project
- And project has CI enabled
- And project has a recent build
-
- Scenario: I download build artifacts
- Given recent build has artifacts available
- When I visit recent build details page
- And I click artifacts download button
- Then download of build artifacts archive starts
-
- Scenario: I browse build artifacts
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- Then I should see content of artifacts archive
- And I should see the build header
-
- Scenario: I browse subdirectory of build artifacts
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- And I click link to subdirectory within build artifacts
- Then I should see content of subdirectory within artifacts archive
- And I should see the directory name in the breadcrumb
-
- Scenario: I browse directory with UTF-8 characters in name
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- And recent build artifacts contain directory with UTF-8 characters
- When I visit recent build details page
- And I click artifacts browse button
- And I navigate to directory with UTF-8 characters in name
- Then I should see content of directory with UTF-8 characters in name
-
- Scenario: I try to browse directory with invalid UTF-8 characters in name
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- And recent build artifacts contain directory with invalid UTF-8 characters
- When I visit recent build details page
- And I click artifacts browse button
- And I navigate to parent directory of directory with invalid name
- Then I should not see directory with invalid name on the list
-
- @javascript
- Scenario: I download a single file from build artifacts
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- And I click a link to file within build artifacts
- Then I see a download link
-
- @javascript
- Scenario: I click on a row in an artifacts table
- Given recent build has artifacts available
- And recent build has artifacts metadata available
- When I visit recent build details page
- And I click artifacts browse button
- And I click a first row within build artifacts table
- Then page with a coresponding path is loading
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
deleted file mode 100644
index 3459cce03f9..00000000000
--- a/features/project/commits/commits.feature
+++ /dev/null
@@ -1,96 +0,0 @@
-@project_commits
-Feature: Project Commits
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's commits page
-
- Scenario: I browse commits list for master branch
- Then I see project commits
- And I should not see button to create a new merge request
- Then I click the "Compare" tab
- And I should not see button to create a new merge request
-
- Scenario: I browse commits list for feature branch without a merge request
- Given I visit commits list page for feature branch
- Then I see feature branch commits
- And I see button to create a new merge request
- Then I click the "Compare" tab
- And I see button to create a new merge request
-
- Scenario: I browse commits list for feature branch with an open merge request
- Given project have an open merge request
- And I visit commits list page for feature branch
- Then I see feature branch commits
- And I should not see button to create a new merge request
- And I should see button to the merge request
- Then I click the "Compare" tab
- And I should not see button to create a new merge request
- And I should see button to the merge request
-
- Scenario: I browse atom feed of commits list for master branch
- Given I click atom feed link
- Then I see commits atom feed
-
- Scenario: I browse commit from list
- Given I click on commit link
- Then I see commit info
- And I see side-by-side diff button
-
- Scenario: I browse commit from list and create a new tag
- Given I click on commit link
- And I click on tag link
- Then I see commit SHA pre-filled
-
- Scenario: I browse commit with ci from list
- Given commit has ci status
- And repository contains ".gitlab-ci.yml" file
- When I click on commit link
- Then I see commit ci info
-
- Scenario: I browse commit with side-by-side diff view
- Given I click on commit link
- And I click side-by-side diff button
- Then I see inline diff button
-
- @javascript
- Scenario: I compare branches without a merge request
- Given I visit compare refs page
- And I fill compare fields with branches
- Then I see compared branches
- And I see button to create a new merge request
-
- @javascript
- Scenario: I compare branches with an open merge request
- Given project have an open merge request
- And I visit compare refs page
- And I fill compare fields with branches
- Then I see compared branches
- And I should not see button to create a new merge request
- And I should see button to the merge request
-
- @javascript
- Scenario: I compare refs
- Given I visit compare refs page
- And I fill compare fields with refs
- Then I see compared refs
- And I unfold diff
- Then I should see additional file lines
-
- Scenario: I browse commits for a specific path
- Given I visit my project's commits page for a specific path
- Then I see breadcrumb links
-
- # TODO: Implement feature in graphs
- #Scenario: I browse commits stats
- #Given I visit my project's commits stats page
- #Then I see commits stats
-
- Scenario: I browse a commit with an image
- Given I visit a commit with an image that changed
- Then The diff links to both the previous and current image
-
- @javascript
- Scenario: I filter commits by message
- When I search "submodules" commits
- Then I should see only "submodules" commits
diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature
deleted file mode 100644
index 35687aac9ea..00000000000
--- a/features/project/commits/diff_comments.feature
+++ /dev/null
@@ -1,96 +0,0 @@
-@project_commits
-Feature: Project Commits Diff Comments
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit project commit page
-
- @javascript
- Scenario: I can comment on a commit diff
- Given I leave a diff comment like "Typo, please fix"
- Then I should see a diff comment saying "Typo, please fix"
-
- @javascript
- Scenario: I can add a diff comment with a single emoji
- Given I open a diff comment form
- And I write a diff comment like ":smile:"
- Then I should see a diff comment with an emoji image
-
- @javascript
- Scenario: I get a temporary form for the first comment on a diff line
- Given I open a diff comment form
- Then I should see a temporary diff comment form
-
- @javascript
- Scenario: I have a cancel button on the diff form
- Given I open a diff comment form
- Then I should see the cancel comment button
-
- @javascript
- Scenario: I can cancel a diff form
- Given I open a diff comment form
- And I cancel the diff comment
- Then I should not see the diff comment form
-
- @javascript
- Scenario: I can't open a second form for a diff line
- Given I open a diff comment form
- And I open a diff comment form
- Then I should only see one diff form
-
- @javascript
- Scenario: I can have multiple forms
- Given I open a diff comment form
- And I write a diff comment like ":-1: I don't like this"
- And I open another diff comment form
- Then I should see a diff comment form with ":-1: I don't like this"
- And I should see an empty diff comment form
-
- @javascript
- Scenario: I can preview multiple forms separately
- Given I preview a diff comment text like "Should fix it :smile:"
- And I preview another diff comment text like "DRY this up"
- Then I should see two separate previews
-
- @javascript
- Scenario: I have a reply button in discussions
- Given I leave a diff comment like "Typo, please fix"
- Then I should see a discussion reply button
-
- @javascript
- Scenario: I can preview with text
- Given I open a diff comment form
- And I write a diff comment like ":-1: I don't like this"
- Then The diff comment preview tab should display rendered Markdown
-
- @javascript
- Scenario: I preview a diff comment
- Given I preview a diff comment text like "Should fix it :smile:"
- Then I should see the diff comment preview
- And I should not see the diff comment text field
-
- @javascript
- Scenario: I can edit after preview
- Given I preview a diff comment text like "Should fix it :smile:"
- Then I should see the diff comment write tab
-
- @javascript
- Scenario: The form gets removed after posting
- Given I preview a diff comment text like "Should fix it :smile:"
- And I submit the diff comment
- Then I should not see the diff comment form
- And I should see a discussion reply button
-
- @javascript
- Scenario: I can add a comment on a side-by-side commit diff (left side)
- Given I open a diff comment form
- And I click side-by-side diff button
- When I leave a diff comment in a parallel view on the left side like "Old comment"
- Then I should see a diff comment on the left side saying "Old comment"
-
- @javascript
- Scenario: I can add a comment on a side-by-side commit diff (right side)
- Given I open a diff comment form
- And I click side-by-side diff button
- When I leave a diff comment in a parallel view on the right side like "New comment"
- Then I should see a diff comment on the right side saying "New comment"
diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature
deleted file mode 100644
index 6f1ed9ff5b6..00000000000
--- a/features/project/deploy_keys.feature
+++ /dev/null
@@ -1,46 +0,0 @@
-Feature: Project Deploy Keys
- Background:
- Given I sign in as a user
- And I own project "Shop"
-
- @javascript
- Scenario: I should see deploy keys list
- Given project has deploy key
- When I visit project deploy keys page
- Then I should see project deploy key
-
- @javascript
- Scenario: I should see project deploy keys
- Given other projects have deploy keys
- When I visit project deploy keys page
- Then I should see other project deploy key
- And I should only see the same deploy key once
-
- @javascript
- Scenario: I should see public deploy keys
- Given public deploy key exists
- When I visit project deploy keys page
- Then I should see public deploy key
-
- @javascript
- Scenario: I add new deploy key
- Given I visit project deploy keys page
- And I submit new deploy key
- Then I should be on deploy keys page
- And I should see newly created deploy key
-
- @javascript
- Scenario: I attach other project deploy key to project
- Given other projects have deploy keys
- And I visit project deploy keys page
- When I click attach deploy key
- Then I should be on deploy keys page
- And I should see newly created deploy key
-
- @javascript
- Scenario: I attach public deploy key to project
- Given public deploy key exists
- And I visit project deploy keys page
- When I click attach deploy key
- Then I should be on deploy keys page
- And I should see newly created deploy key
diff --git a/features/project/ff_merge_requests.feature b/features/project/ff_merge_requests.feature
deleted file mode 100644
index 39035d551d1..00000000000
--- a/features/project/ff_merge_requests.feature
+++ /dev/null
@@ -1,41 +0,0 @@
-Feature: Project Ff Merge Requests
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" have "Bug NS-05" open merge request with diffs inside
- And merge request "Bug NS-05" is mergeable
-
- @javascript
- Scenario: I do ff-only merge for rebased branch
- Given ff merge enabled
- And merge request "Bug NS-05" is rebased
- When I visit merge request page "Bug NS-05"
- Then I should see ff-only merge button
- When I accept this merge request
- Then I should see merged request
-
- @javascript
- Scenario: I do ff-only merge for merged branch
- Given ff merge enabled
- And merge request "Bug NS-05" merged target
- When I visit merge request page "Bug NS-05"
- Then I should see ff-only merge button
- When I accept this merge request
- Then I should see merged request
-
- @javascript
- Scenario: I do rebase before ff-only merge
- Given ff merge enabled
- And rebase before merge enabled
- When I visit merge request page "Bug NS-05"
- Then I should see rebase button
- When I press rebase button
- Then I should see rebase in progress message
-
- @javascript
- Scenario: I do rebase before regular merge
- Given rebase before merge enabled
- When I visit merge request page "Bug NS-05"
- Then I should see rebase button
- When I press rebase button
- Then I should see rebase in progress message
diff --git a/features/project/find_file.feature b/features/project/find_file.feature
deleted file mode 100644
index ae8fa245923..00000000000
--- a/features/project/find_file.feature
+++ /dev/null
@@ -1,42 +0,0 @@
-@dashboard
-Feature: Project Find File
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's files page
-
- @javascript
- Scenario: Navigate to find file by shortcut
- Given I press "t"
- Then I should see "find file" page
-
- Scenario: Navigate to find file
- Given I click Find File button
- Then I should see "find file" page
-
- @javascript
- Scenario: I search file
- Given I visit project find file page
- And I fill in file find with "change"
- Then I should not see ".gitignore" in files
- And I should not see ".gitmodules" in files
- And I should see "CHANGELOG" in files
- And I should not see "VERSION" in files
-
- @javascript
- Scenario: I search file that not exist
- Given I visit project find file page
- And I fill in file find with "asdfghjklqwertyuizxcvbnm"
- Then I should not see ".gitignore" in files
- And I should not see ".gitmodules" in files
- And I should not see "CHANGELOG" in files
- And I should not see "VERSION" in files
-
- @javascript
- Scenario: I search file that partially matches
- Given I visit project find file page
- And I fill in file find with "git"
- Then I should see ".gitignore" in files
- And I should see ".gitmodules" in files
- And I should not see "CHANGELOG" in files
- And I should not see "VERSION" in files
diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature
deleted file mode 100644
index 9809b0ea0fe..00000000000
--- a/features/project/forked_merge_requests.feature
+++ /dev/null
@@ -1,51 +0,0 @@
-Feature: Project Forked Merge Requests
- Background:
- Given I sign in as a user
- And I am a member of project "Shop"
- And I have a project forked off of "Shop" called "Forked Shop"
-
- @javascript
- Scenario: I submit new unassigned merge request to a forked project
- Given I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- And I fill out a "Merge Request On Forked Project" merge request
- And I submit the merge request
- Then I should see merge request "Merge Request On Forked Project"
-
- # TODO: Improve it so it does not fail randomly
- #
- #@javascript
- #Scenario: I can edit a forked merge request
- #Given I visit project "Forked Shop" merge requests page
- #And I click link "New Merge Request"
- #And I fill out a "Merge Request On Forked Project" merge request
- #And I submit the merge request
- #And I should see merge request "Merge Request On Forked Project"
- #And I click link edit "Merge Request On Forked Project"
- #Then I see the edit page prefilled for "Merge Request On Forked Project"
- #And I update the merge request title
- #And I save the merge request
- #Then I should see the edited merge request
-
- Scenario: I cannot submit an invalid merge request
- Given I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- And I fill out an invalid "Merge Request On Forked Project" merge request
- Then I should see validation errors
-
- @javascript
- Scenario: Merge request should target fork repository by default
- Given I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- Then the target repository should be the original repository
-
- @javascript
- Scenario: I see the users in the target project for a new merge request
- Given I sign in as an admin
- And I have a project forked off of "Shop" called "Forked Shop"
- Then I visit project "Forked Shop" merge requests page
- And I click link "New Merge Request"
- And I fill out a "Merge Request On Forked Project" merge request
- When I click "Assign to" dropdown"
- Then I should see the target project ID in the input selector
- And I should see the users from the target project ID
diff --git a/features/project/issues/references.feature b/features/project/issues/references.feature
deleted file mode 100644
index 4ae2d653337..00000000000
--- a/features/project/issues/references.feature
+++ /dev/null
@@ -1,33 +0,0 @@
-@project_issues
-Feature: Project Issues References
- Background:
- Given I sign in as "John Doe"
- And public project "Community"
- And "John Doe" owns public project "Community"
- And project "Community" has "Community issue" open issue
- And I logout
- And I sign in as "Mary Jane"
- And private project "Enterprise"
- And "Mary Jane" owns private project "Enterprise"
- And project "Enterprise" has "Enterprise issue" open issue
- And project "Enterprise" has "Enterprise fix" open merge request
- And I visit issue page "Enterprise issue"
- And I leave a comment referencing issue "Community issue"
- And I visit merge request page "Enterprise fix"
- And I leave a comment referencing issue "Community issue"
- And I logout
-
- @javascript
- Scenario: Viewing the public issue as a "John Doe"
- Given I sign in as "John Doe"
- When I visit issue page "Community issue"
- Then I should not see any related merge requests
- And I should see no notes at all
-
- @javascript
- Scenario: Viewing the public issue as "Mary Jane"
- Given I sign in as "Mary Jane"
- When I visit issue page "Community issue"
- Then I should see the "Enterprise fix" related merge request
- And I should see a note linking to "Enterprise fix" merge request
- And I should see a note linking to "Enterprise issue" issue
diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature
deleted file mode 100644
index 571612261a9..00000000000
--- a/features/project/merge_requests/references.feature
+++ /dev/null
@@ -1,31 +0,0 @@
-@project_merge_requests
-Feature: Project Merge Requests References
- Background:
- Given I sign in as "John Doe"
- And public project "Community"
- And "John Doe" owns public project "Community"
- And project "Community" has "Community fix" open merge request
- And I logout
- And I sign in as "Mary Jane"
- And private project "Enterprise"
- And "Mary Jane" owns private project "Enterprise"
- And project "Enterprise" has "Enterprise issue" open issue
- And project "Enterprise" has "Enterprise fix" open merge request
- And I visit issue page "Enterprise issue"
- And I leave a comment referencing issue "Community fix"
- And I visit merge request page "Enterprise fix"
- And I leave a comment referencing issue "Community fix"
- And I logout
-
- @javascript
- Scenario: Viewing the public issue as a "John Doe"
- Given I sign in as "John Doe"
- When I visit issue page "Community fix"
- Then I should see no notes at all
-
- @javascript
- Scenario: Viewing the public issue as "Mary Jane"
- Given I sign in as "Mary Jane"
- When I visit issue page "Community fix"
- And I should see a note linking to "Enterprise fix" merge request
- And I should see a note linking to "Enterprise issue" issue
diff --git a/features/project/source/markdown_render.feature b/features/project/source/markdown_render.feature
deleted file mode 100644
index fe4466ad241..00000000000
--- a/features/project/source/markdown_render.feature
+++ /dev/null
@@ -1,147 +0,0 @@
-Feature: Project Source Markdown Render
- Background:
- Given I sign in as a user
- And I own project "Delta"
- And I visit markdown branch
-
- # Tree README
-
- @javascript
- Scenario: Tree view should have correct links in README
- Given I go directory which contains README file
- And I click on a relative link in README
- Then I should see the correct markdown
-
- @javascript
- Scenario: I browse files from markdown branch
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on Gitlab API in README
- Then I should see correct document rendered
-
- @javascript
- Scenario: I view README in markdown branch
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on Rake tasks in README
- Then I should see correct directory rendered
-
- @javascript
- Scenario: I view README in markdown branch to see reference links to directory
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on GitLab API doc directory in README
- Then I should see correct doc/api directory rendered
-
- @javascript
- Scenario: I view README in markdown branch to see reference links to file
- Then I should see files from repository in markdown
- And I should see rendered README which contains correct links
- And I click on Maintenance in README
- Then I should see correct maintenance file rendered
-
- @javascript
- Scenario: README headers should have header links
- Then I should see rendered README which contains correct links
- And Header "Application details" should have correct id and link
-
- # Blob
-
- @javascript
- Scenario: I navigate to doc directory to view documentation in markdown
- And I navigate to the doc/api/README
- And I see correct file rendered
- And I click on users in doc/api/README
- Then I should see the correct document file
-
- @javascript
- Scenario: I navigate to doc directory to view user doc in markdown
- And I navigate to the doc/api/README
- And I see correct file rendered
- And I click on raketasks in doc/api/README
- Then I should see correct directory rendered
-
- @javascript
- Scenario: I navigate to doc directory to view user doc in markdown
- And I navigate to the doc/api/README
- And Header "GitLab API" should have correct id and link
-
- # Markdown branch
-
- @javascript
- Scenario: I browse files from markdown branch
- When I visit markdown branch
- Then I should see files from repository in markdown branch
- And I should see rendered README which contains correct links
- And I click on Gitlab API in README
- Then I should see correct document rendered for markdown branch
-
- @javascript
- Scenario: I browse directory from markdown branch
- When I visit markdown branch
- Then I should see files from repository in markdown branch
- And I should see rendered README which contains correct links
- And I click on Rake tasks in README
- Then I should see correct directory rendered for markdown branch
-
- @javascript
- Scenario: I navigate to doc directory to view documentation in markdown branch
- When I visit markdown branch
- And I navigate to the doc/api/README
- And I see correct file rendered in markdown branch
- And I click on users in doc/api/README
- Then I should see the users document file in markdown branch
-
- @javascript
- Scenario: I navigate to doc directory to view user doc in markdown branch
- When I visit markdown branch
- And I navigate to the doc/api/README
- And I see correct file rendered in markdown branch
- And I click on raketasks in doc/api/README
- Then I should see correct directory rendered for markdown branch
-
- @javascript
- Scenario: Tree markdown links view empty urls should have correct urls
- When I visit markdown branch
- Then The link with text "empty" should have url "tree/markdown"
- When I visit markdown branch "README.md" blob
- Then The link with text "empty" should have url "blob/markdown/README.md"
- When I visit markdown branch "d" tree
- Then The link with text "empty" should have url "tree/markdown/d"
- When I visit markdown branch "d/README.md" blob
- Then The link with text "empty" should have url "blob/markdown/d/README.md"
-
- # "ID" means "#id" on the tests below, because we are unable to escape the hash sign.
- # which Spinach interprets as the start of a comment.
- @javascript
- Scenario: All markdown links with ids should have correct urls
- When I visit markdown branch
- Then The link with text "ID" should have url "tree/markdownID"
- Then The link with text "/ID" should have url "tree/markdownID"
- Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
- Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
- When I visit markdown branch "README.md" blob
- Then The link with text "ID" should have url "blob/markdown/README.mdID"
- Then The link with text "/ID" should have url "blob/markdown/README.mdID"
- Then The link with text "README.mdID" should have url "blob/markdown/README.mdID"
- Then The link with text "d/README.mdID" should have url "blob/markdown/d/README.mdID"
-
- # Wiki
-
- Scenario: I create a wiki page with different links
- Given I go to wiki page
- And I add various links to the wiki page
- Then Wiki page should have added links
- And I click on test link
- Then I see new wiki page named test
- When I go back to wiki page home
- And I click on GitLab API doc link
- Then I see Gitlab API document
- When I go back to wiki page home
- And I click on Rake tasks link
- Then I see Rake tasks directory
-
- Scenario: Wiki headers should have should have ids generated for them.
- Given I go to wiki page
- And I add a header to the wiki page
- Then Wiki header should have correct id and link
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
deleted file mode 100644
index 97bcca7730b..00000000000
--- a/features/steps/group/members.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-class Spinach::Features::GroupMembers < Spinach::FeatureSteps
- include WaitForRequests
- include SharedAuthentication
- include SharedPaths
- include SharedGroup
- include SharedUser
-
- step 'I should see user "John Doe" in team list' do
- expect(group_members_list).to have_content("John Doe")
- end
-
- step 'I should not see user "Mary Jane" in team list' do
- expect(group_members_list).not_to have_content("Mary Jane")
- end
-
- step 'I click on the "Remove User From Group" button for "John Doe"' do
- find(:css, '.project-members-page li', text: "John Doe").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I click on the "Remove User From Group" button for "Mary Jane"' do
- find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "John Doe"' do
- expect(find(:css, '.project-members-page li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "Mary Jane"' do
- expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I change the "Mary Jane" role to "Developer"' do
- member = mary_jane_member
-
- page.within "#group_member_#{member.id}" do
- click_button member.human_access
-
- page.within '.dropdown-menu' do
- click_link 'Developer'
- end
-
- wait_for_requests
- end
- end
-
- step 'I should see "Mary Jane" as "Developer"' do
- member = mary_jane_member
-
- page.within "#group_member_#{member.id}" do
- expect(page).to have_content "Developer"
- end
- end
-
- private
-
- def mary_jane_member
- user = User.find_by(name: "Mary Jane")
- owned_group.members.find_by(user_id: user.id)
- end
-
- def group_members_list
- find(".panel .content-list")
- end
-end
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
deleted file mode 100644
index f8eb0f01de8..00000000000
--- a/features/steps/profile/notifications.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
-
- step 'I visit profile notifications page' do
- visit profile_notifications_path
- end
-
- step 'I should see global notifications settings' do
- expect(page).to have_content "Notifications"
- end
-
- step 'I select Mention setting from dropdown' do
- first(:link, "On mention").click
- end
-
- step 'I should see Notification saved message' do
- expect(page).to have_content 'On mention'
- end
-end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
deleted file mode 100644
index 4b72355b125..00000000000
--- a/features/steps/project/builds/artifacts.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedBuilds
- include RepoHelpers
- include WaitForRequests
-
- step 'I click artifacts download button' do
- click_link 'Download'
- end
-
- step 'I click artifacts browse button' do
- click_link 'Browse'
- expect(page).not_to have_selector('.build-sidebar')
- end
-
- step 'I should see content of artifacts archive' do
- page.within('.tree-table') do
- expect(page).to have_no_content '..'
- expect(page).to have_content 'other_artifacts_0.1.2'
- expect(page).to have_content 'ci_artifacts.txt'
- expect(page).to have_content 'rails_sample.jpg'
- end
- end
-
- step 'I should see the build header' do
- page.within('.build-header') do
- expect(page).to have_content "Job ##{@build.id} in pipeline ##{@pipeline.id} for #{@pipeline.short_sha}"
- end
- end
-
- step 'I click link to subdirectory within build artifacts' do
- page.within('.tree-table') { click_link 'other_artifacts_0.1.2' }
- end
-
- step 'I should see content of subdirectory within artifacts archive' do
- page.within('.tree-table') do
- expect(page).to have_content '..'
- expect(page).to have_content 'another-subdirectory'
- expect(page).to have_content 'doc_sample.txt'
- end
- end
-
- step 'I should see the directory name in the breadcrumb' do
- page.within('.repo-breadcrumb') do
- expect(page).to have_content 'other_artifacts_0.1.2'
- end
- end
-
- step 'recent build artifacts contain directory with UTF-8 characters' do
- # metadata fixture contains relevant directory
- end
-
- step 'I navigate to directory with UTF-8 characters in name' do
- page.within('.tree-table') { click_link 'tests_encoding' }
- page.within('.tree-table') { click_link 'utf8 test dir ✓' }
- end
-
- step 'I should see content of directory with UTF-8 characters in name' do
- page.within('.tree-table') do
- expect(page).to have_content '..'
- expect(page).to have_content 'regular_file_2'
- end
- end
-
- step 'recent build artifacts contain directory with invalid UTF-8 characters' do
- # metadata fixture contains relevant directory
- end
-
- step 'I navigate to parent directory of directory with invalid name' do
- page.within('.tree-table') { click_link 'tests_encoding' }
- end
-
- step 'I should not see directory with invalid name on the list' do
- page.within('.tree-table') do
- expect(page).to have_no_content('non-utf8-dir')
- end
- end
-
- step 'I click a link to file within build artifacts' do
- page.within('.tree-table') { find_link('ci_artifacts.txt').click }
- wait_for_requests
- end
-
- step 'I see a download link' do
- expect(page).to have_link 'download it'
- end
-
- step 'I click a first row within build artifacts table' do
- row = first('tr[data-link]')
- @row_path = row['data-link']
- row.click
- end
-
- step 'page with a coresponding path is loading' do
- expect(current_path).to eq @row_path
- end
-end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
deleted file mode 100644
index 3ecd4c8b672..00000000000
--- a/features/steps/project/commits/branches.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'I click link "All"' do
- click_link "All"
- end
-
- step 'I click link "Protected"' do
- click_link "Protected"
- end
-
- step 'I click new branch link' do
- click_link "New branch"
- end
-
- step 'I submit new branch form with invalid name' do
- fill_in 'branch_name', with: '1.0 stable'
- page.find("body").click # defocus the branch_name input
- select_branch('master')
- click_button 'Create branch'
- end
-
- def select_branch(branch_name)
- find('.git-revision-dropdown-toggle').click
-
- page.within '#new-branch-form .dropdown-menu' do
- click_link branch_name
- end
- end
-end
diff --git a/features/steps/project/commits/comments.rb b/features/steps/project/commits/comments.rb
deleted file mode 100644
index 3d4d8ad6368..00000000000
--- a/features/steps/project/commits/comments.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Spinach::Features::ProjectCommitsComments < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedNote
- include SharedPaths
- include SharedProject
-end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
deleted file mode 100644
index 959cf7d3e54..00000000000
--- a/features/steps/project/commits/commits.rb
+++ /dev/null
@@ -1,192 +0,0 @@
-class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include SharedDiffNote
- include RepoHelpers
-
- step 'I see project commits' do
- commit = @project.repository.commit
- expect(page).to have_content(@project.name)
- expect(page).to have_content(commit.message[0..20])
- expect(page).to have_content(commit.short_id)
- end
-
- step 'I click atom feed link' do
- click_link "Commits feed"
- end
-
- step 'I see commits atom feed' do
- commit = @project.repository.commit
- expect(response_headers['Content-Type']).to have_content("application/atom+xml")
- expect(body).to have_selector("title", text: "#{@project.name}:master commits")
- expect(body).to have_selector("author email", text: commit.author_email)
- expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r\n"))
- end
-
- step 'I click on tag link' do
- click_link "Tag"
- end
-
- step 'I see commit SHA pre-filled' do
- expect(page).to have_selector("input[value='#{sample_commit.id}']")
- end
-
- step 'I click on commit link' do
- visit project_commit_path(@project, sample_commit.id)
- end
-
- step 'I see commit info' do
- expect(page).to have_content sample_commit.message
- expect(page).to have_content "Showing #{sample_commit.files_changed_count} changed files"
- end
-
- step 'I fill compare fields with branches' do
- select_using_dropdown('from', 'feature')
- select_using_dropdown('to', 'master')
-
- click_button 'Compare'
- end
-
- step 'I fill compare fields with refs' do
- select_using_dropdown('from', sample_commit.parent_id, true)
- select_using_dropdown('to', sample_commit.id, true)
-
- click_button "Compare"
- end
-
- step 'I unfold diff' do
- @diff = first('.js-unfold')
- @diff.click
- sleep 2
- end
-
- step 'I should see additional file lines' do
- page.within @diff.query_scope do
- expect(first('.new_line').text).not_to have_content "..."
- end
- end
-
- step 'I see compared refs' do
- expect(page).to have_content "Commits (1)"
- expect(page).to have_content "Showing 2 changed files"
- end
-
- step 'I visit commits list page for feature branch' do
- visit project_commits_path(@project, 'feature', { limit: 5 })
- end
-
- step 'I see feature branch commits' do
- commit = @project.repository.commit('0b4bc9a')
- expect(page).to have_content(@project.name)
- expect(page).to have_content(commit.message[0..12])
- expect(page).to have_content(commit.short_id)
- end
-
- step 'project have an open merge request' do
- create(:merge_request,
- title: 'Feature',
- source_project: @project,
- source_branch: 'feature',
- target_branch: 'master',
- author: @project.users.first
- )
- end
-
- step 'I click the "Compare" tab' do
- click_link('Compare')
- end
-
- step 'I fill compare fields with branches' do
- select_using_dropdown('from', 'master')
- select_using_dropdown('to', 'feature')
-
- click_button 'Compare'
- end
-
- step 'I see compared branches' do
- expect(page).to have_content 'Commits (1)'
- expect(page).to have_content 'Showing 1 changed file with 5 additions and 0 deletions'
- end
-
- step 'I see button to create a new merge request' do
- expect(page).to have_link 'Create merge request'
- end
-
- step 'I should not see button to create a new merge request' do
- expect(page).not_to have_link 'Create merge request'
- end
-
- step 'I should see button to the merge request' do
- merge_request = MergeRequest.find_by(title: 'Feature')
- expect(page).to have_link "View open merge request", href: project_merge_request_path(@project, merge_request)
- end
-
- step 'I see breadcrumb links' do
- expect(page).to have_selector('ul.breadcrumb')
- expect(page).to have_selector('ul.breadcrumb a', count: 4)
- end
-
- step 'I see commits stats' do
- expect(page).to have_content 'Top 50 Committers'
- expect(page).to have_content 'Committers'
- expect(page).to have_content 'Total commits'
- expect(page).to have_content 'Authors'
- end
-
- step 'I visit a commit with an image that changed' do
- visit project_commit_path(@project, sample_image_commit.id)
- end
-
- step 'The diff links to both the previous and current image' do
- links = page.all('.file-actions a')
- expect(links[0]['href']).to match %r{blob/#{sample_image_commit.old_blob_id}}
- expect(links[1]['href']).to match %r{blob/#{sample_image_commit.new_blob_id}}
- end
-
- step 'I see inline diff button' do
- expect(page).to have_content "Inline"
- end
-
- step 'I click side-by-side diff button' do
- find('#parallel-diff-btn').click
- end
-
- step 'commit has ci status' do
- @project.enable_ci
- @pipeline = create(:ci_pipeline, project: @project, sha: sample_commit.id)
- create(:ci_build, pipeline: @pipeline)
- end
-
- step 'repository contains ".gitlab-ci.yml" file' do
- allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(String.new)
- end
-
- step 'I see commit ci info' do
- expect(page).to have_content "Pipeline ##{@pipeline.id} pending"
- end
-
- step 'I search "submodules" commits' do
- fill_in 'commits-search', with: 'submodules'
- end
-
- step 'I should see only "submodules" commits' do
- expect(page).to have_content "More submodules"
- expect(page).not_to have_content "Change some files"
- end
-
- def select_using_dropdown(dropdown_type, selection, is_commit = false)
- dropdown = find(".js-compare-#{dropdown_type}-dropdown")
- dropdown.find(".compare-dropdown-toggle").click
- dropdown.find('.dropdown-menu', visible: true)
- dropdown.fill_in("Filter by Git revision", with: selection)
-
- if is_commit
- dropdown.find('input[type="search"]').send_keys(:return)
- else
- find_link(selection, visible: true).click
- end
-
- dropdown.find('.dropdown-menu', visible: false)
- end
-end
diff --git a/features/steps/project/commits/diff_comments.rb b/features/steps/project/commits/diff_comments.rb
deleted file mode 100644
index b9d8cf2c5a5..00000000000
--- a/features/steps/project/commits/diff_comments.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Spinach::Features::ProjectCommitsDiffComments < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedDiffNote
- include SharedPaths
- include SharedProject
-end
diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb
deleted file mode 100644
index 60fa232672e..00000000000
--- a/features/steps/project/create.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedUser
-
- step 'fill project form with valid data' do
- fill_in 'project_path', with: 'Empty'
- page.within '#content-body' do
- click_button "Create project"
- end
- end
-
- step 'I should see project page' do
- expect(page).to have_content "Empty"
- expect(current_path).to eq project_path(Project.last)
- end
-
- step 'I should see empty project instructions' do
- expect(page).to have_content "git init"
- expect(page).to have_content "git remote"
- expect(page).to have_content Project.last.url_to_repo
- end
-end
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
deleted file mode 100644
index 9db31522c5c..00000000000
--- a/features/steps/project/deploy_keys.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'project has deploy key' do
- create(:deploy_keys_project, project: @project)
- end
-
- step 'I should see project deploy key' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_content deploy_key.title
- end
- end
-
- step 'I should see other project deploy key' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_content other_deploy_key.title
- end
- end
-
- step 'I should see public deploy key' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_content public_deploy_key.title
- end
- end
-
- step 'I click \'New Deploy Key\'' do
- click_link 'New deploy key'
- end
-
- step 'I submit new deploy key' do
- fill_in "deploy_key_title", with: "laptop"
- fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
- click_button "Add key"
- end
-
- step 'I should be on deploy keys page' do
- expect(current_path).to eq project_settings_repository_path(@project)
- end
-
- step 'I should see newly created deploy key' do
- @project.reload
- page.within(find('.deploy-keys')) do
- expect(page).to have_content(deploy_key.title)
- end
- end
-
- step 'other projects have deploy keys' do
- @second_project = create(:project, namespace: create(:group))
- @second_project.add_master(current_user)
- create(:deploy_keys_project, project: @second_project)
-
- @third_project = create(:project, namespace: create(:group))
- @third_project.add_master(current_user)
- create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
- end
-
- step 'I should only see the same deploy key once' do
- page.within(find('.deploy-keys')) do
- expect(page).to have_selector('ul li', count: 1)
- end
- end
-
- step 'public deploy key exists' do
- create(:deploy_key, public: true)
- end
-
- step 'I click attach deploy key' do
- page.within(find('.deploy-keys')) do
- click_button 'Enable'
- expect(page).not_to have_selector('.fa-spinner')
- end
- end
-
- protected
-
- def deploy_key
- @project.deploy_keys.last
- end
-
- def other_deploy_key
- @second_project.deploy_keys.last
- end
-
- def public_deploy_key
- DeployKey.are_public.last
- end
-end
diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb
deleted file mode 100644
index 27efcfd65b6..00000000000
--- a/features/steps/project/ff_merge_requests.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedNote
- include SharedPaths
- include SharedMarkdown
- include SharedDiffNote
- include SharedUser
- include WaitForRequests
-
- step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
- create(:merge_request_with_diffs,
- title: "Bug NS-05",
- source_project: project,
- target_project: project,
- author: project.users.first)
- end
-
- step 'merge request is mergeable' do
- expect(page).to have_button 'Merge'
- end
-
- step 'I should see ff-only merge button' do
- expect(page).to have_content "Fast-forward merge without a merge commit"
- expect(page).to have_button 'Merge'
- end
-
- step 'merge request "Bug NS-05" is mergeable' do
- merge_request.mark_as_mergeable
- end
-
- step 'I accept this merge request' do
- page.within '.mr-state-widget' do
- click_button "Merge"
- end
- end
-
- step 'I should see merged request' do
- page.within '.status-box' do
- expect(page).to have_content "Merged"
- wait_for_requests
- end
- end
-
- step 'ff merge enabled' do
- project = merge_request.target_project
- project.merge_requests_ff_only_enabled = true
- project.save!
- end
-
- step 'I should see rebase button' do
- expect(page).to have_button "Rebase"
- end
-
- step 'merge request "Bug NS-05" is rebased' do
- merge_request.source_branch = 'flatten-dir'
- merge_request.target_branch = 'improve/awesome'
- merge_request.reload_diff
- merge_request.save!
- end
-
- step 'merge request "Bug NS-05" merged target' do
- merge_request.source_branch = 'merged-target'
- merge_request.target_branch = 'improve/awesome'
- merge_request.reload_diff
- merge_request.save!
- end
-
- step 'rebase before merge enabled' do
- project = merge_request.target_project
- project.merge_requests_rebase_enabled = true
- project.save!
- end
-
- step 'I press rebase button' do
- click_button "Rebase"
- end
-
- step "I should see rebase in progress message" do
- expect(page).to have_content("Rebase in progress")
- end
-
- def merge_request
- @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
- end
-end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
deleted file mode 100644
index fd51ee1a316..00000000000
--- a/features/steps/project/forked_merge_requests.rb
+++ /dev/null
@@ -1,139 +0,0 @@
-class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedNote
- include SharedPaths
- include Select2Helper
- include WaitForRequests
- include ProjectForksHelper
-
- step 'I am a member of project "Shop"' do
- @project = ::Project.find_by(name: "Shop")
- @project ||= create(:project, :repository, name: "Shop")
- @project.add_reporter(@user)
- end
-
- step 'I have a project forked off of "Shop" called "Forked Shop"' do
- @forked_project = fork_project(@project, @user,
- namespace: @user.namespace,
- repository: true)
- end
-
- step 'I click link "New Merge Request"' do
- page.within '#content-body' do
- page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
- end
- end
-
- step 'I should see merge request "Merge Request On Forked Project"' do
- expect(@project.merge_requests.size).to be >= 1
- @merge_request = @project.merge_requests.last
- expect(current_path).to eq project_merge_request_path(@project, @merge_request)
- expect(@merge_request.title).to eq "Merge Request On Forked Project"
- expect(@merge_request.source_project).to eq @forked_project
- expect(@merge_request.source_branch).to eq "fix"
- expect(@merge_request.target_branch).to eq "master"
- expect(page).to have_content @forked_project.full_path
- expect(page).to have_content @project.full_path
- expect(page).to have_content @merge_request.source_branch
- expect(page).to have_content @merge_request.target_branch
-
- wait_for_requests
- end
-
- step 'I fill out a "Merge Request On Forked Project" merge request' do
- expect(page).to have_content('Source branch')
- expect(page).to have_content('Target branch')
-
- first('.js-source-project').click
- first('.dropdown-source-project a', text: @forked_project.full_path)
-
- first('.js-target-project').click
- first('.dropdown-target-project a', text: @project.full_path)
-
- first('.js-source-branch').click
- wait_for_requests
- first('.dropdown-source-branch .dropdown-content a', text: 'fix').click
-
- click_button "Compare branches and continue"
-
- expect(page).to have_css("h3.page-title", text: "New Merge Request")
-
- page.within 'form#new_merge_request' do
- fill_in "merge_request_title", with: "Merge Request On Forked Project"
- end
- end
-
- step 'I submit the merge request' do
- click_button "Submit merge request"
- end
-
- step 'I update the merge request title' do
- fill_in "merge_request_title", with: "An Edited Forked Merge Request"
- end
-
- step 'I save the merge request' do
- click_button "Save changes"
- end
-
- step 'I should see the edited merge request' do
- expect(page).to have_content "An Edited Forked Merge Request"
- expect(@project.merge_requests.size).to be >= 1
- @merge_request = @project.merge_requests.last
- expect(current_path).to eq project_merge_request_path(@project, @merge_request)
- expect(@merge_request.source_project).to eq @forked_project
- expect(@merge_request.source_branch).to eq "fix"
- expect(@merge_request.target_branch).to eq "master"
- expect(page).to have_content @forked_project.full_path
- expect(page).to have_content @project.full_path
- expect(page).to have_content @merge_request.source_branch
- expect(page).to have_content @merge_request.target_branch
- end
-
- step 'I should see last push widget' do
- expect(page).to have_content "You pushed to new_design"
- expect(page).to have_link "Create Merge Request"
- end
-
- step 'I click link edit "Merge Request On Forked Project"' do
- find("#edit_merge_request").click
- end
-
- step 'I see the edit page prefilled for "Merge Request On Forked Project"' do
- expect(current_path).to eq edit_project_merge_request_path(@project, @merge_request)
- expect(page).to have_content "Edit merge request #{@merge_request.to_reference}"
- expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project"
- end
-
- step 'I fill out an invalid "Merge Request On Forked Project" merge request' do
- expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s
- expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s
- expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil
- expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master"
- click_button "Compare branches"
- end
-
- step 'I should see validation errors' do
- expect(page).to have_content "You must select source and target branch"
- end
-
- step 'the target repository should be the original repository' do
- expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}"
- end
-
- step 'I click "Assign to" dropdown"' do
- click_button 'Assignee'
- end
-
- step 'I should see the target project ID in the input selector' do
- expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}"
- end
-
- step 'I should see the users from the target project ID' do
- page.within '.dropdown-menu-user' do
- expect(page).to have_content 'Unassigned'
- expect(page).to have_content current_user.name
- expect(page).to have_content @project.users.first.name
- end
- end
-end
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
deleted file mode 100644
index b467af53c98..00000000000
--- a/features/steps/project/issues/filter_labels.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include Select2Helper
-
- step 'I should see "Bugfix1" in issues list' do
- page.within ".issues-list" do
- expect(page).to have_content "Bugfix1"
- end
- end
-
- step 'I should see "Bugfix2" in issues list' do
- page.within ".issues-list" do
- expect(page).to have_content "Bugfix2"
- end
- end
-
- step 'I should not see "Bugfix2" in issues list' do
- page.within ".issues-list" do
- expect(page).not_to have_content "Bugfix2"
- end
- end
-
- step 'I should not see "Feature1" in issues list' do
- page.within ".issues-list" do
- expect(page).not_to have_content "Feature1"
- end
- end
-
- step 'I click "dropdown close button"' do
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
- end
-
- step 'I click link "feature"' do
- page.within ".labels-filter" do
- click_link "feature"
- end
- end
-
- step 'project "Shop" has issue "Bugfix1" with labels: "bug", "feature"' do
- project = Project.find_by(name: "Shop")
- issue = create(:issue, title: "Bugfix1", project: project)
- issue.labels << project.labels.find_by(title: 'bug')
- issue.labels << project.labels.find_by(title: 'feature')
- end
-
- step 'project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"' do
- project = Project.find_by(name: "Shop")
- issue = create(:issue, title: "Bugfix2", project: project)
- issue.labels << project.labels.find_by(title: 'bug')
- issue.labels << project.labels.find_by(title: 'enhancement')
- end
-
- step 'project "Shop" has issue "Feature1" with labels: "feature"' do
- project = Project.find_by(name: "Shop")
- issue = create(:issue, title: "Feature1", project: project)
- issue.labels << project.labels.find_by(title: 'feature')
- end
-end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
deleted file mode 100644
index baa78c23203..00000000000
--- a/features/steps/project/issues/issues.rb
+++ /dev/null
@@ -1,175 +0,0 @@
-class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedProject
- include SharedNote
- include SharedPaths
- include SharedMarkdown
- include SharedUser
-
- step 'I should not see "Release 0.3" in issues' do
- expect(page).not_to have_content "Release 0.3"
- end
-
- step 'I click link "Closed"' do
- find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click
- end
-
- step 'I should see "Release 0.3" in issues' do
- expect(page).to have_content "Release 0.3"
- end
-
- step 'I should not see "Release 0.4" in issues' do
- expect(page).not_to have_content "Release 0.4"
- end
-
- step 'I click link "All"' do
- find('.issues-state-filters [data-state="all"] span', text: 'All').click
- # Waits for load
- expect(find('.issues-state-filters > .active')).to have_content 'All'
- end
-
- step 'I should see issue "Tweet control"' do
- expect(page).to have_content "Tweet control"
- end
-
- step 'I click "author" dropdown' do
- page.find('.js-author-search').click
- sleep 1
- end
-
- step 'I see current user as the first user' do
- expect(page).to have_selector('.dropdown-content', visible: true)
- users = page.all('.dropdown-menu-author .dropdown-content li a')
- expect(users[0].text).to eq 'Any Author'
- expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}"
- end
-
- step 'I click link "500 error on profile"' do
- click_link "500 error on profile"
- end
-
- step 'I should see label \'bug\' with issue' do
- page.within '.issuable-show-labels' do
- expect(page).to have_content 'bug'
- end
- end
-
- step 'I fill in issue search with "Re"' do
- filter_issue "Re"
- end
-
- step 'I fill in issue search with "Bu"' do
- filter_issue "Bu"
- end
-
- step 'I fill in issue search with ".3"' do
- filter_issue ".3"
- end
-
- step 'I fill in issue search with "Something"' do
- filter_issue "Something"
- end
-
- step 'I fill in issue search with ""' do
- filter_issue ""
- end
-
- step 'project "Shop" has milestone "v2.2"' do
- milestone = create(:milestone, title: "v2.2", project: project)
-
- 3.times { create(:issue, project: project, milestone: milestone) }
- end
-
- step 'project "Shop" has milestone "v3.0"' do
- milestone = create(:milestone, title: "v3.0", project: project)
-
- 3.times { create(:issue, project: project, milestone: milestone) }
- end
-
- When 'I select milestone "v3.0"' do
- select "v3.0", from: "milestone_id"
- end
-
- step 'I should see selected milestone with title "v3.0"' do
- issues_milestone_selector = "#issue_milestone_id_chzn > a"
- expect(find(issues_milestone_selector)).to have_content("v3.0")
- end
-
- When 'I select first assignee from "Shop" project' do
- first_assignee = project.users.first
- select first_assignee.name, from: "assignee_id"
- end
-
- step 'I should see first assignee from "Shop" as selected assignee' do
- issues_assignee_selector = "#issue_assignee_id_chzn > a"
-
- assignee_name = project.users.first.name
- expect(find(issues_assignee_selector)).to have_content(assignee_name)
- end
-
- step 'The list should be sorted by "Least popular"' do
- page.within '.issues-list' do
- page.within 'li.issue:nth-child(1)' do
- expect(page).to have_content 'Tweet control'
- expect(page).to have_content '1 2'
- end
-
- page.within 'li.issue:nth-child(2)' do
- expect(page).to have_content 'Release 0.4'
- expect(page).to have_content '2 1'
- end
-
- page.within 'li.issue:nth-child(3)' do
- expect(page).to have_content 'Bugfix'
- expect(page).not_to have_content '0 0'
- end
- end
- end
-
- When 'I visit empty project page' do
- project = Project.find_by(name: 'Empty Project')
- visit project_path(project)
- end
-
- When "I visit project \"Community\" issues page" do
- project = Project.find_by(name: 'Community')
- visit project_issues_path(project)
- end
-
- step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do
- create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project)
- end
-
- step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do
- create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project)
- end
-
- step 'I fill in issue search with \'Description for issue1\'' do
- filter_issue 'Description for issue'
- end
-
- step 'I fill in issue search with \'issue1\'' do
- filter_issue 'issue1'
- end
-
- step 'I fill in issue search with \'Rock and roll\'' do
- filter_issue 'Rock and roll'
- end
-
- step 'I should see \'Bugfix1\' in issues' do
- expect(page).to have_content 'Bugfix1'
- end
-
- step 'I should see \'Feature1\' in issues' do
- expect(page).to have_content 'Feature1'
- end
-
- step 'I should not see \'Bugfix1\' in issues' do
- expect(page).not_to have_content 'Bugfix1'
- end
-
- def filter_issue(text)
- fill_in 'issuable_search', with: text
- end
-end
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
deleted file mode 100644
index 30927306a4f..00000000000
--- a/features/steps/project/issues/milestones.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include SharedMarkdown
-
- step 'project "Shop" has milestone "v2.2"' do
- project = Project.find_by(name: "Shop")
- milestone = create(:milestone,
- title: "v2.2",
- project: project,
- description: "# Description header"
- )
- 3.times { create(:issue, project: project, milestone: milestone) }
- end
-
- When 'I click link "All Issues"' do
- click_link 'All Issues'
- end
-end
diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb
deleted file mode 100644
index 69e8b5cbde5..00000000000
--- a/features/steps/project/issues/references.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedNote
- include SharedProject
- include SharedUser
-end
diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb
deleted file mode 100644
index ab2ae6847a2..00000000000
--- a/features/steps/project/merge_requests/references.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedIssuable
- include SharedNote
- include SharedProject
- include SharedUser
-end
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
deleted file mode 100644
index 461160b8430..00000000000
--- a/features/steps/project/project_find_file.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedProjectTab
-
- step 'I press "t"' do
- find('body').native.send_key('t')
- end
-
- step 'I click Find File button' do
- click_link 'Find file'
- end
-
- step 'I should see "find file" page' do
- ensure_active_main_tab('Repository')
- expect(page).to have_selector('.file-finder-holder', count: 1)
- end
-
- step 'I fill in Find by path with "git"' do
- ensure_active_main_tab('Repository')
- expect(page).to have_selector('.file-finder-holder', count: 1)
- end
-
- step 'I fill in file find with "git"' do
- find_file "git"
- end
-
- step 'I fill in file find with "change"' do
- find_file "change"
- end
-
- step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do
- find_file "asdfghjklqwertyuizxcvbnm"
- end
-
- step 'I should see "VERSION" in files' do
- expect(page).to have_content("VERSION")
- end
-
- step 'I should not see "VERSION" in files' do
- expect(page).not_to have_content("VERSION")
- end
-
- step 'I should see "CHANGELOG" in files' do
- expect(page).to have_content("CHANGELOG")
- end
-
- step 'I should not see "CHANGELOG" in files' do
- expect(page).not_to have_content("CHANGELOG")
- end
-
- step 'I should see ".gitmodules" in files' do
- expect(page).to have_content(".gitmodules")
- end
-
- step 'I should not see ".gitmodules" in files' do
- expect(page).not_to have_content(".gitmodules")
- end
-
- step 'I should see ".gitignore" in files' do
- expect(page).to have_content(".gitignore")
- end
-
- step 'I should not see ".gitignore" in files' do
- expect(page).not_to have_content(".gitignore")
- end
-
- def find_file(text)
- fill_in 'file_find', with: text
- end
-end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
deleted file mode 100644
index afaad4b255e..00000000000
--- a/features/steps/project/source/browse_files.rb
+++ /dev/null
@@ -1,435 +0,0 @@
-# coding: utf-8
-class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include RepoHelpers
- include WaitForRequests
-
- step "I don't have write access" do
- @project = create(:project, :repository, name: "Other Project", path: "other-project")
- @project.add_reporter(@user)
- visit project_tree_path(@project, root_ref)
- end
-
- step 'I should see files from repository' do
- expect(page).to have_content "VERSION"
- expect(page).to have_content ".gitignore"
- expect(page).to have_content "LICENSE"
- end
-
- step 'I should see files from repository for "6d39438"' do
- expect(current_path).to eq project_tree_path(@project, "6d39438")
- expect(page).to have_content ".gitignore"
- expect(page).to have_content "LICENSE"
- end
-
- step 'I see the ".gitignore"' do
- expect(page).to have_content '.gitignore'
- end
-
- step 'I don\'t see the ".gitignore"' do
- expect(page).not_to have_content '.gitignore'
- end
-
- step 'I click on ".gitignore" file in repo' do
- click_link ".gitignore"
- end
-
- step 'I should see its content' do
- wait_for_requests
- expect(page).to have_content old_gitignore_content
- end
-
- step 'I should see its new content' do
- wait_for_requests
- expect(page).to have_content new_gitignore_content
- end
-
- step 'I click link "Raw"' do
- click_link 'Open raw'
- end
-
- step 'I should see raw file content' do
- expect(source).to eq '' # Body is filled in by gitlab-workhorse
- end
-
- step 'I click button "Edit"' do
- find('.js-edit-blob').click
- end
-
- step 'I cannot see the edit button' do
- expect(page).not_to have_link 'edit'
- end
-
- step 'I click button "Fork"' do
- click_link 'Fork'
- end
-
- step 'I edit code' do
- expect(page).to have_selector('.file-editor')
- set_new_content
- end
-
- step 'I fill the new file name' do
- fill_in :file_name, with: new_file_name
- end
-
- step 'I fill the new branch name' do
- fill_in :branch_name, with: 'new_branch_name', visible: true
- end
-
- step 'I fill the new file name with a new directory' do
- fill_in :file_name, with: new_file_name_with_directory
- end
-
- step 'I fill the commit message' do
- fill_in :commit_message, with: 'New commit message', visible: true
- end
-
- step 'I click link "Diff"' do
- click_link 'Preview changes'
- end
-
- step 'I click on "Commit changes"' do
- click_button 'Commit changes'
- end
-
- step 'I click on "Changes" tab' do
- click_link 'Changes'
- end
-
- step 'I click on "Create directory"' do
- click_button 'Create directory'
- end
-
- step 'I click on "Delete"' do
- click_on 'Delete'
- end
-
- step 'I click on "Delete file"' do
- click_button 'Delete file'
- end
-
- step 'I click on "Replace"' do
- click_on "Replace"
- end
-
- step 'I click on "Replace file"' do
- click_button 'Replace file'
- end
-
- step 'I see diff' do
- expect(page).to have_css '.line_holder.new'
- end
-
- step 'I click on "New file" link in repo' do
- find('.add-to-tree').click
- click_link 'New file'
- expect(page).to have_selector('.file-editor')
- end
-
- step 'I click on "Upload file" link in repo' do
- find('.add-to-tree').click
- click_link 'Upload file'
- end
-
- step 'I click on "New directory" link in repo' do
- find('.add-to-tree').click
- click_link 'New directory'
- end
-
- step 'I fill the new directory name' do
- fill_in :dir_name, with: new_dir_name
- end
-
- step 'I fill an existing directory name' do
- fill_in :dir_name, with: 'files'
- end
-
- step 'I can see new file page' do
- expect(page).to have_content "New File"
- expect(page).to have_content "Commit message"
- end
-
- step 'I click on "Upload file"' do
- click_button 'Upload file'
- end
-
- step 'I can see the new commit message' do
- expect(page).to have_content "New commit message"
- end
-
- step 'I upload a new text file' do
- drop_in_dropzone test_text_file
- end
-
- step 'I fill the upload file commit message' do
- page.within('#modal-upload-blob') do
- fill_in :commit_message, with: 'New commit message'
- end
- end
-
- step 'I replace it with a text file' do
- drop_in_dropzone test_text_file
- end
-
- step 'I fill the replace file commit message' do
- page.within('#modal-upload-blob') do
- fill_in :commit_message, with: 'Replacement file commit message'
- end
- end
-
- step 'I can see the replacement commit message' do
- expect(page).to have_content "Replacement file commit message"
- end
-
- step 'I can see the new text file' do
- expect(page).to have_content "Lorem ipsum dolor sit amet"
- expect(page).to have_content "Sed ut perspiciatis unde omnis"
- end
-
- step 'I click on files directory' do
- click_link 'files'
- end
-
- step 'I click on History link' do
- click_link 'History'
- end
-
- step 'I see Browse dir link' do
- expect(page).to have_link 'Browse Directory'
- expect(page).not_to have_link 'Browse Code'
- end
-
- step 'I click on readme file' do
- page.within '.tree-table' do
- click_link 'README.md'
- end
- end
-
- step 'I see Browse file link' do
- expect(page).to have_link 'Browse File'
- expect(page).not_to have_link 'Browse Files'
- end
-
- step 'I see Browse code link' do
- expect(page).to have_link 'Browse Files'
- expect(page).not_to have_link 'Browse Directory'
- end
-
- step 'I click on Permalink' do
- click_link 'Permalink'
- end
-
- step 'I am redirected to the files URL' do
- expect(current_path).to eq project_tree_path(@project, 'master')
- end
-
- step 'I am redirected to the ".gitignore"' do
- expect(current_path).to eq(project_blob_path(@project, 'master/.gitignore'))
- end
-
- step 'I am redirected to the permalink URL' do
- expect(current_path).to(
- eq(project_blob_path(@project,
- @project.repository.commit.sha +
- '/.gitignore'))
- )
- end
-
- step 'I am redirected to the new file' do
- expect(current_path).to eq(
- project_blob_path(@project, 'master/' + new_file_name))
- end
-
- step 'I am redirected to the new file with directory' do
- expect(current_path).to eq(
- project_blob_path(@project, 'master/' + new_file_name_with_directory))
- end
-
- step 'I am redirected to the new merge request page' do
- expect(current_path).to eq(project_new_merge_request_path(@project))
- end
-
- step "I am redirected to the fork's new merge request page" do
- fork = @user.fork_of(@project)
- expect(current_path).to eq(project_new_merge_request_path(fork))
- end
-
- step 'I am redirected to the root directory' do
- expect(current_path).to eq(
- project_tree_path(@project, 'master'))
- end
-
- step "I don't see the permalink link" do
- expect(page).not_to have_link('permalink')
- end
-
- step 'I see "Unable to create directory"' do
- expect(page).to have_content('A directory with this name already exists')
- end
-
- step 'I see "Path can contain only..."' do
- expect(page).to have_content('Path can contain only')
- end
-
- step 'I see a commit error message' do
- expect(page).to have_content('Your changes could not be committed')
- end
-
- step "I switch ref to 'test'" do
- first('.js-project-refs-dropdown').click
-
- page.within '.project-refs-form' do
- click_link "'test'"
- end
- end
-
- step "I switch ref to fix" do
- first('.js-project-refs-dropdown').click
-
- page.within '.project-refs-form' do
- click_link 'fix'
- end
- end
-
- step "I see the ref 'test' has been selected" do
- expect(page).to have_selector '.dropdown-toggle-text', text: "'test'"
- end
-
- step "I visit the 'test' tree" do
- visit project_tree_path(@project, "'test'")
- end
-
- step "I visit the fix tree" do
- visit project_tree_path(@project, "fix/.testdir")
- end
-
- step 'I see the commit data' do
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
- end
-
- step 'I see the commit data for a directory with a leading dot' do
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
- end
-
- step 'I click on "files/lfs/lfs_object.iso" file in repo' do
- allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
- visit project_tree_path(@project, "lfs")
- click_link 'files'
- click_link "lfs"
- click_link "lfs_object.iso"
- end
-
- step 'I should see download link and object size' do
- expect(page).to have_content 'Download (1.5 MB)'
- end
-
- step 'I should not see lfs pointer details' do
- expect(page).not_to have_content 'version https://git-lfs.github.com/spec/v1'
- expect(page).not_to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
- expect(page).not_to have_content 'size 1575078'
- end
-
- step 'I should see buttons for allowed commands' do
- page.within '.content' do
- expect(page).to have_link 'Download'
- expect(page).to have_content 'History'
- expect(page).to have_content 'Permalink'
- expect(page).not_to have_content 'Edit'
- expect(page).not_to have_content 'Blame'
- expect(page).to have_content 'Delete'
- expect(page).to have_content 'Replace'
- end
- end
-
- step 'I should see a Fork/Cancel combo' do
- expect(page).to have_link 'Fork'
- expect(page).to have_button 'Cancel'
- end
-
- step 'I should see a notice about a new fork having been created' do
- expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
- end
-
- # SVG files
- step 'I upload a new SVG file' do
- drop_in_dropzone test_svg_file
- end
-
- step 'I visit the SVG file' do
- visit project_blob_path(@project, 'new_branch_name/logo_sample.svg')
- end
-
- step 'I can see the new rendered SVG image' do
- expect(page).to have_css('.file-content img')
- end
-
- private
-
- def set_new_content
- find('#editor')
- execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
- end
-
- # Content of the gitignore file on the seed repository.
- def old_gitignore_content
- '*.rbc'
- end
-
- # Constant value that differs from the content
- # of the gitignore of the seed repository.
- def new_gitignore_content
- old_gitignore_content + 'a'
- end
-
- # Constant value that is a valid filename and
- # not a filename present at root of the seed repository.
- def new_file_name
- 'not_a_file.md'
- end
-
- # Constant value that is a valid filename with directory and
- # not a filename present at root of the seed repository.
- def new_file_name_with_directory
- 'foo/bar/baz.txt'
- end
-
- # Constant value that is a valid directory and
- # not a directory present at root of the seed repository.
- def new_dir_name
- 'new_dir/subdir'
- end
-
- def drop_in_dropzone(file_path)
- # Generate a fake input selector
- page.execute_script <<-JS
- var fakeFileInput = window.$('<input/>').attr(
- {id: 'fakeFileInput', type: 'file'}
- ).appendTo('body');
- JS
- # Attach the file to the fake input selector with Capybara
- attach_file("fakeFileInput", file_path)
- # Add the file to a fileList array and trigger the fake drop event
- page.execute_script <<-JS
- var fileList = [$('#fakeFileInput')[0].files[0]];
- var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
- $('.dropzone')[0].dropzone.listeners[0].events.drop(e);
- JS
- end
-
- def test_text_file
- File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')
- end
-
- def test_image_file
- File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
- end
-
- def test_svg_file
- File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')
- end
-end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
deleted file mode 100644
index db99c179439..00000000000
--- a/features/steps/project/source/markdown_render.rb
+++ /dev/null
@@ -1,317 +0,0 @@
-# If you need to modify the existing seed repository for your tests,
-# it is recommended that you make the changes on the `markdown` branch of the seed project repository,
-# which should only be used by tests in this file. See `/spec/factories.rb#project` for more info.
-class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedMarkdown
- include WaitForRequests
-
- step 'I own project "Delta"' do
- @project = ::Project.find_by(name: "Delta")
- @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
- @project.add_master(@user)
- end
-
- step 'I should see files from repository in markdown' do
- expect(current_path).to eq project_tree_path(@project, "markdown")
- expect(page).to have_content "README.md"
- expect(page).to have_content "CHANGELOG"
- end
-
- step 'I should see rendered README which contains correct links' do
- expect(page).to have_content "Welcome to GitLab GitLab is a free project and repository management application"
- expect(page).to have_link "GitLab API doc"
- expect(page).to have_link "GitLab API website"
- expect(page).to have_link "Rake tasks"
- expect(page).to have_link "backup and restore procedure"
- expect(page).to have_link "GitLab API doc directory"
- expect(page).to have_link "Maintenance"
- end
-
- step 'I click on Gitlab API in README' do
- click_link "GitLab API doc"
- end
-
- step 'I should see correct document rendered' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "All API requests require authentication"
- end
-
- step 'I click on Rake tasks in README' do
- click_link "Rake tasks"
- end
-
- step 'I should see correct directory rendered' do
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/raketasks")
- expect(page).to have_content "backup_restore.md"
- expect(page).to have_content "maintenance.md"
- end
-
- step 'I click on GitLab API doc directory in README' do
- click_link "GitLab API doc directory"
- end
-
- step 'I should see correct doc/api directory rendered' do
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/api")
- expect(page).to have_content "README.md"
- expect(page).to have_content "users.md"
- end
-
- step 'I click on Maintenance in README' do
- click_link "Maintenance"
- end
-
- step 'I should see correct maintenance file rendered' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/raketasks/maintenance.md")
- wait_for_requests
- expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production"
- end
-
- step 'I click on link "empty" in the README' do
- page.within('.readme-holder') do
- click_link "empty"
- end
- end
-
- step 'I click on link "id" in the README' do
- page.within('.readme-holder') do
- click_link "#id"
- end
- end
-
- step 'I navigate to the doc/api/README' do
- page.within '.tree-table' do
- click_link "doc"
- end
-
- page.within '.tree-table' do
- click_link "api"
- end
-
- wait_for_requests
-
- page.within '.tree-table' do
- click_link "README.md"
- end
- end
-
- step 'I see correct file rendered' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "Contents"
- expect(page).to have_link "Users"
- expect(page).to have_link "Rake tasks"
- end
-
- step 'I click on users in doc/api/README' do
- click_link "Users"
- end
-
- step 'I should see the correct document file' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
- expect(page).to have_content "Get a list of users."
- end
-
- step 'I click on raketasks in doc/api/README' do
- click_link "Rake tasks"
- end
-
- # Markdown branch
-
- When 'I visit markdown branch' do
- visit project_tree_path(@project, "markdown")
- wait_for_requests
- end
-
- When 'I visit markdown branch "README.md" blob' do
- visit project_blob_path(@project, "markdown/README.md")
- end
-
- When 'I visit markdown branch "d" tree' do
- visit project_tree_path(@project, "markdown/d")
- end
-
- When 'I visit markdown branch "d/README.md" blob' do
- visit project_blob_path(@project, "markdown/d/README.md")
- end
-
- step 'I should see files from repository in markdown branch' do
- expect(current_path).to eq project_tree_path(@project, "markdown")
- expect(page).to have_content "README.md"
- expect(page).to have_content "CHANGELOG"
- end
-
- step 'I see correct file rendered in markdown branch' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "Contents"
- expect(page).to have_link "Users"
- expect(page).to have_link "Rake tasks"
- end
-
- step 'I should see correct document rendered for markdown branch' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/README.md")
- wait_for_requests
- expect(page).to have_content "All API requests require authentication"
- end
-
- step 'I should see correct directory rendered for markdown branch' do
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/raketasks")
- expect(page).to have_content "backup_restore.md"
- expect(page).to have_content "maintenance.md"
- end
-
- step 'I should see the users document file in markdown branch' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
- expect(page).to have_content "Get a list of users."
- end
-
- # Expected link contents
-
- step 'The link with text "empty" should have url "tree/markdown"' do
- wait_for_requests
- find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown")
- end
-
- step 'The link with text "empty" should have url "blob/markdown/README.md"' do
- find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md")
- end
-
- step 'The link with text "empty" should have url "tree/markdown/d"' do
- find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown/d")
- end
-
- step 'The link with text "empty" should have '\
- 'url "blob/markdown/d/README.md"' do
- find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/d/README.md")
- end
-
- step 'The link with text "ID" should have url "tree/markdownID"' do
- find('a', text: /^#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
- end
-
- step 'The link with text "/ID" should have url "tree/markdownID"' do
- find('a', text: %r{^/#id$})['href'] == current_host + project_tree_path(@project, "markdown") + '#id'
- end
-
- step 'The link with text "README.mdID" '\
- 'should have url "blob/markdown/README.mdID"' do
- find('a', text: /^README.md#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
- end
-
- step 'The link with text "d/README.mdID" should have '\
- 'url "blob/markdown/d/README.mdID"' do
- find('a', text: %r{^d/README.md#id$})['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id'
- end
-
- step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do
- wait_for_requests
- find('a', text: /^#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
- end
-
- step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do
- find('a', text: %r{^/#id$})['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id'
- end
-
- # Wiki
-
- step 'I go to wiki page' do
- first(:link, "Wiki").click
- expect(current_path).to eq project_wiki_path(@project, "home")
- end
-
- step 'I add various links to the wiki page' do
- fill_in "wiki[content]", with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n"
- fill_in "wiki[message]", with: "Adding links to wiki"
- page.within '.wiki-form' do
- click_button "Create page"
- end
- end
-
- step 'Wiki page should have added links' do
- expect(current_path).to eq project_wiki_path(@project, "home")
- expect(page).to have_content "test GitLab API doc Rake tasks"
- end
-
- step 'I add a header to the wiki page' do
- fill_in "wiki[content]", with: "# Wiki header\n"
- fill_in "wiki[message]", with: "Add header to wiki"
- page.within '.wiki-form' do
- click_button "Create page"
- end
- end
-
- step 'Wiki header should have correct id and link' do
- header_should_have_correct_id_and_link(1, 'Wiki header', 'wiki-header')
- end
-
- step 'I click on test link' do
- click_link "test"
- end
-
- step 'I see new wiki page named test' do
- expect(current_path).to eq project_wiki_path(@project, "test")
-
- page.within(:css, ".nav-text") do
- expect(page).to have_content "Test"
- expect(page).to have_content "Create Page"
- end
- end
-
- When 'I go back to wiki page home' do
- visit project_wiki_path(@project, "home")
- expect(current_path).to eq project_wiki_path(@project, "home")
- end
-
- step 'I click on GitLab API doc link' do
- click_link "GitLab API"
- end
-
- step 'I see Gitlab API document' do
- expect(current_path).to eq project_wiki_path(@project, "api")
-
- page.within(:css, ".nav-text") do
- expect(page).to have_content "Create"
- expect(page).to have_content "Api"
- end
- end
-
- step 'I click on Rake tasks link' do
- click_link "Rake tasks"
- end
-
- step 'I see Rake tasks directory' do
- expect(current_path).to eq project_wiki_path(@project, "raketasks")
-
- page.within(:css, ".nav-text") do
- expect(page).to have_content "Create"
- expect(page).to have_content "Rake"
- end
- end
-
- step 'I go directory which contains README file' do
- visit project_tree_path(@project, "markdown/doc/api")
- expect(current_path).to eq project_tree_path(@project, "markdown/doc/api")
- end
-
- step 'I click on a relative link in README' do
- click_link "Users"
- end
-
- step 'I should see the correct markdown' do
- expect(current_path).to eq project_blob_path(@project, "markdown/doc/api/users.md")
- wait_for_requests
- expect(page).to have_content "List users"
- end
-
- step 'Header "Application details" should have correct id and link' do
- wait_for_requests
- header_should_have_correct_id_and_link(2, 'Application details', 'application-details')
- end
-
- step 'Header "GitLab API" should have correct id and link' do
- header_should_have_correct_id_and_link(1, 'GitLab API', 'gitlab-api')
- end
-end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
deleted file mode 100644
index 104d024fee2..00000000000
--- a/features/steps/shared/active_tab.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module SharedActiveTab
- include Spinach::DSL
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- def ensure_active_main_tab(content)
- expect(find('.sidebar-top-level-items > li.active')).to have_content(content)
- end
-
- def ensure_active_sub_tab(content)
- expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content)
- end
-
- def ensure_active_sub_nav(content)
- expect(find('.layout-nav .controls li.active')).to have_content(content)
- end
-
- step 'no other main tabs should be active' do
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- end
-
- step 'no other sub tabs should be active' do
- expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
- end
-
- step 'no other sub navs should be active' do
- expect(page).to have_selector('.layout-nav .controls li.active', count: 1)
- end
-end
diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb
deleted file mode 100644
index ac0a1764147..00000000000
--- a/features/steps/shared/admin.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module SharedAdmin
- include Spinach::DSL
-
- step 'there are projects in system' do
- 2.times { create(:project, :repository) }
- end
-
- step 'system has users' do
- 2.times { create(:user) }
- end
-end
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
deleted file mode 100644
index 97fac595d8e..00000000000
--- a/features/steps/shared/authentication.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-require Rails.root.join('features', 'support', 'login_helpers')
-
-module SharedAuthentication
- include Spinach::DSL
- include LoginHelpers
-
- step 'I sign in as a user' do
- sign_out(@user) if @user
-
- @user = create(:user)
- sign_in(@user)
- end
-
- step 'I sign in via the UI' do
- gitlab_sign_in(create(:user))
- end
-
- step 'I sign in as an admin' do
- sign_out(@user) if @user
-
- @user = create(:admin)
- sign_in(@user)
- end
-
- step 'I sign in as "John Doe"' do
- gitlab_sign_in(user_exists("John Doe"))
- end
-
- step 'I sign in as "Mary Jane"' do
- gitlab_sign_in(user_exists("Mary Jane"))
- end
-
- step 'I should be redirected to sign in page' do
- expect(current_path).to eq new_user_session_path
- end
-
- step "I logout" do
- gitlab_sign_out
- end
-
- step "I logout directly" do
- gitlab_sign_out
- end
-
- def current_user
- @user || User.reorder(nil).first
- end
-
- private
-
- def gitlab_sign_in(user)
- visit new_user_session_path
-
- fill_in "user_login", with: user.email
- fill_in "user_password", with: "12345678"
- check 'user_remember_me'
- click_button "Sign in"
-
- @user = user
- end
-
- def gitlab_sign_out
- return unless @user
-
- if Capybara.current_driver == Capybara.javascript_driver
- find('.header-user-dropdown-toggle').click
- click_link 'Sign out'
- expect(page).to have_button('Sign in')
- else
- sign_out(@user)
- end
-
- @user = nil
- end
-end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
deleted file mode 100644
index c2197584d8d..00000000000
--- a/features/steps/shared/builds.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-module SharedBuilds
- include Spinach::DSL
-
- step 'project has CI enabled' do
- @project.enable_ci
- end
-
- step 'project has coverage enabled' do
- @project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/)
- end
-
- step 'project has a recent build' do
- @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
- @build = create(:ci_build, :running, :coverage, :trace_artifact, pipeline: @pipeline)
- end
-
- step 'recent build is successful' do
- @build.success
- end
-
- step 'recent build failed' do
- @build.drop
- end
-
- step 'project has another build that is running' do
- create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run')
- end
-
- step 'I visit recent build details page' do
- visit project_job_path(@project, @build)
- end
-
- step 'recent build has artifacts available' do
- artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
- archive = fixture_file_upload(artifacts, 'application/zip')
- @build.update_attributes(legacy_artifacts_file: archive)
- end
-
- step 'recent build has artifacts metadata available' do
- metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
- gzip = fixture_file_upload(metadata, 'application/x-gzip')
- @build.update_attributes(legacy_artifacts_metadata: gzip)
- end
-
- step 'recent build has a build trace' do
- @build.trace.set('job trace')
- end
-
- step 'download of build artifacts archive starts' do
- expect(page.response_headers['Content-Type']).to eq 'application/zip'
- expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
- end
-end
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
deleted file mode 100644
index aa32528a7ca..00000000000
--- a/features/steps/shared/diff_note.rb
+++ /dev/null
@@ -1,237 +0,0 @@
-module SharedDiffNote
- include Spinach::DSL
- include RepoHelpers
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- step 'I cancel the diff comment' do
- page.within(diff_file_selector) do
- find(".js-close-discussion-note-form").click
- end
- end
-
- step 'I delete a diff comment' do
- find('.note').hover
- find(".js-note-delete").click
- end
-
- step 'I haven\'t written any diff comment text' do
- page.within(diff_file_selector) do
- fill_in "note[note]", with: ""
- end
- end
-
- step 'I leave a diff comment like "Typo, please fix"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
-
- page.within("form[data-line-code='#{sample_commit.line_code}']") do
- fill_in "note[note]", with: "Typo, please fix"
- find(".js-comment-button").click
- end
- end
- end
-
- step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do
- click_parallel_diff_line(sample_commit.del_line_code, 'old')
- page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.del_line_code}']") do
- fill_in "note[note]", with: "Old comment"
- find(".js-comment-button").click
- end
- end
-
- step 'I leave a diff comment in a parallel view on the right side like "New comment"' do
- click_parallel_diff_line(sample_commit.line_code, 'new')
- page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do
- fill_in "note[note]", with: "New comment"
- find(".js-comment-button").click
- end
- end
-
- step 'I preview a diff comment text like "Should fix it :smile:"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
-
- page.within("form[data-line-code='#{sample_commit.line_code}']") do
- fill_in "note[note]", with: "Should fix it :smile:"
- find('.js-md-preview-button').click
- end
- end
- end
-
- step 'I preview another diff comment text like "DRY this up"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.del_line_code)
-
- page.within("form[data-line-code='#{sample_commit.del_line_code}']") do
- fill_in "note[note]", with: "DRY this up"
- find('.js-md-preview-button').click
- end
- end
- end
-
- step 'I open a diff comment form' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
- end
- end
-
- step 'I open another diff comment form' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.del_line_code)
- end
- end
-
- step 'I write a diff comment like ":-1: I don\'t like this"' do
- page.within(diff_file_selector) do
- fill_in "note[note]", with: ":-1: I don\'t like this"
- end
- end
-
- step 'I write a diff comment like ":smile:"' do
- page.within(diff_file_selector) do
- click_diff_line(sample_commit.line_code)
-
- page.within("form[data-line-code='#{sample_commit.line_code}']") do
- fill_in 'note[note]', with: ':smile:'
- click_button('Comment')
- end
- end
- end
-
- step 'I submit the diff comment' do
- page.within(diff_file_selector) do
- click_button("Comment")
- end
- end
-
- step 'I should not see the diff comment form' do
- page.within(diff_file_selector) do
- expect(page).not_to have_css("form.new_note")
- end
- end
-
- step 'The diff comment preview tab should say there is nothing to do' do
- page.within(diff_file_selector) do
- find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_content('Nothing to preview.')
- end
- end
-
- step 'I should not see the diff comment text field' do
- page.within(diff_file_selector) do
- expect(find('.js-note-text')).not_to be_visible
- end
- end
-
- step 'I should only see one diff form' do
- page.within(diff_file_selector) do
- expect(page).to have_css("form.new-note", count: 1)
- end
- end
-
- step 'I should see a diff comment form with ":-1: I don\'t like this"' do
- page.within(diff_file_selector) do
- expect(page).to have_field("note[note]", with: ":-1: I don\'t like this")
- end
- end
-
- step 'I should see a diff comment saying "Typo, please fix"' do
- page.within("#{diff_file_selector} .note") do
- expect(page).to have_content("Typo, please fix")
- end
- end
-
- step 'I should see a diff comment on the left side saying "Old comment"' do
- page.within("#{diff_file_selector} .notes_content.parallel.old") do
- expect(page).to have_content("Old comment")
- end
- end
-
- step 'I should see a diff comment on the right side saying "New comment"' do
- page.within("#{diff_file_selector} .notes_content.parallel.new") do
- expect(page).to have_content("New comment")
- end
- end
-
- step 'I should see a discussion reply button' do
- page.within(diff_file_selector) do
- expect(page).to have_button('Reply...')
- end
- end
-
- step 'I should see a temporary diff comment form' do
- page.within(diff_file_selector) do
- expect(page).to have_css(".js-temp-notes-holder form.new-note")
- end
- end
-
- step 'I should see an empty diff comment form' do
- page.within(diff_file_selector) do
- expect(page).to have_field("note[note]", with: "")
- end
- end
-
- step 'I should see the cancel comment button' do
- page.within("#{diff_file_selector} form") do
- expect(page).to have_css(".js-close-discussion-note-form", text: "Cancel")
- end
- end
-
- step 'I should see the diff comment preview' do
- page.within("#{diff_file_selector} form") do
- expect(page).to have_css('.js-md-preview', visible: true)
- end
- end
-
- step 'I should see the diff comment write tab' do
- page.within(diff_file_selector) do
- expect(page).to have_css('.js-md-write-button', visible: true)
- end
- end
-
- step 'The diff comment preview tab should display rendered Markdown' do
- page.within(diff_file_selector) do
- find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
- end
- end
-
- step 'I should see two separate previews' do
- page.within(diff_file_selector) do
- expect(page).to have_css('.js-md-preview', visible: true, count: 2)
- expect(page).to have_content('Should fix it')
- expect(page).to have_content('DRY this up')
- end
- end
-
- step 'I should see a diff comment with an emoji image' do
- page.within("#{diff_file_selector} .note") do
- expect(page).to have_xpath("//gl-emoji[@data-name='smile']")
- end
- end
-
- step 'I click side-by-side diff button' do
- find('#parallel-diff-btn').click
- end
-
- step 'I see side-by-side diff button' do
- expect(page).to have_content "Side-by-side"
- end
-
- def diff_file_selector
- '.diff-file:nth-of-type(1)'
- end
-
- def click_diff_line(code)
- find(".line_holder[id='#{code}'] button").click
- end
-
- def click_parallel_diff_line(code, line_type)
- find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover
- find(".line_holder.parallel button[data-line-code='#{code}']").click
- end
-end
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
deleted file mode 100644
index 0a0588346b1..00000000000
--- a/features/steps/shared/group.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module SharedGroup
- include Spinach::DSL
-
- step 'current user is developer of group "Owned"' do
- is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER)
- end
-
- step '"John Doe" is owner of group "Owned"' do
- is_member_of("John Doe", "Owned", Gitlab::Access::OWNER)
- end
-
- step '"John Doe" is guest of group "Guest"' do
- is_member_of("John Doe", "Guest", Gitlab::Access::GUEST)
- end
-
- step '"Mary Jane" is owner of group "Owned"' do
- is_member_of("Mary Jane", "Owned", Gitlab::Access::OWNER)
- end
-
- step '"Mary Jane" is guest of group "Owned"' do
- is_member_of("Mary Jane", "Owned", Gitlab::Access::GUEST)
- end
-
- step '"Mary Jane" is guest of group "Guest"' do
- is_member_of("Mary Jane", "Guest", Gitlab::Access::GUEST)
- end
-
- step 'I should see group "TestGroup"' do
- expect(page).to have_content "TestGroup"
- end
-
- step 'I should not see group "TestGroup"' do
- expect(page).not_to have_content "TestGroup"
- end
-
- protected
-
- def is_member_of(username, groupname, role)
- user = User.find_by(name: username) || create(:user, name: username)
- group = Group.find_by(name: groupname) || create(:group, name: groupname)
- group.add_user(user, role)
- project ||= create(:project, :repository, namespace: group)
- create(:closed_issue_event, project: project)
- project.add_master(user)
- end
-
- def owned_group
- @owned_group ||= Group.find_by(name: "Owned")
- end
-end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
deleted file mode 100644
index a9174efd334..00000000000
--- a/features/steps/shared/issuable.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-module SharedIssuable
- include Spinach::DSL
-
- def edit_issuable
- find('.js-issuable-edit', visible: true).click
- end
-
- step 'project "Community" has "Community issue" open issue' do
- create_issuable_for_project(
- project_name: 'Community',
- title: 'Community issue'
- )
- end
-
- step 'project "Community" has "Community fix" open merge request' do
- create_issuable_for_project(
- project_name: 'Community',
- type: :merge_request,
- title: 'Community fix'
- )
- end
-
- step 'project "Enterprise" has "Enterprise issue" open issue' do
- create_issuable_for_project(
- project_name: 'Enterprise',
- title: 'Enterprise issue'
- )
- end
-
- step 'project "Enterprise" has "Enterprise fix" open merge request' do
- create_issuable_for_project(
- project_name: 'Enterprise',
- type: :merge_request,
- title: 'Enterprise fix'
- )
- end
-
- step 'I leave a comment referencing issue "Community issue"' do
- leave_reference_comment(
- issuable: Issue.find_by(title: 'Community issue'),
- from_project_name: 'Enterprise'
- )
- end
-
- step 'I leave a comment referencing issue "Community fix"' do
- leave_reference_comment(
- issuable: MergeRequest.find_by(title: 'Community fix'),
- from_project_name: 'Enterprise'
- )
- end
-
- step 'I visit issue page "Enterprise issue"' do
- issue = Issue.find_by(title: 'Enterprise issue')
- visit project_issue_path(issue.project, issue)
- end
-
- step 'I visit merge request page "Enterprise fix"' do
- mr = MergeRequest.find_by(title: 'Enterprise fix')
- visit project_merge_request_path(mr.target_project, mr)
- end
-
- step 'I visit issue page "Community issue"' do
- issue = Issue.find_by(title: 'Community issue')
- visit project_issue_path(issue.project, issue)
- end
-
- step 'I visit issue page "Community fix"' do
- mr = MergeRequest.find_by(title: 'Community fix')
- visit project_merge_request_path(mr.target_project, mr)
- end
-
- step 'I should not see any related merge requests' do
- page.within '.issue-details' do
- expect(page).not_to have_content('#merge-requests .merge-requests-title')
- end
- end
-
- step 'I should see the "Enterprise fix" related merge request' do
- page.within '#merge-requests .merge-requests-title' do
- expect(page).to have_content('1 Related Merge Request')
- end
-
- page.within '#merge-requests ul' do
- expect(page).to have_content('Enterprise fix')
- end
- end
-
- step 'I should see a note linking to "Enterprise fix" merge request' do
- visible_note(
- issuable: MergeRequest.find_by(title: 'Enterprise fix'),
- from_project_name: 'Community',
- user_name: 'Mary Jane'
- )
- end
-
- step 'I should see a note linking to "Enterprise issue" issue' do
- visible_note(
- issuable: Issue.find_by(title: 'Enterprise issue'),
- from_project_name: 'Community',
- user_name: 'Mary Jane'
- )
- end
-
- step 'I click link "Edit" for the merge request' do
- edit_issuable
- end
-
- step 'I sort the list by "Least popular"' do
- find('button.dropdown-toggle').click
-
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
- click_link 'Least popular'
- end
- end
-
- step 'I click link "Next" in the sidebar' do
- page.within '.issuable-sidebar' do
- click_link 'Next'
- end
- end
-
- def create_issuable_for_project(project_name:, title:, type: :issue)
- project = Project.find_by(name: project_name)
-
- attrs = {
- title: title,
- author: project.users.first,
- description: '# Description header'
- }
-
- case type
- when :issue
- attrs[:project] = project
- when :merge_request
- attrs.merge!(
- source_project: project,
- target_project: project,
- source_branch: 'fix',
- target_branch: 'master'
- )
- end
-
- create(type, attrs)
- end
-
- def leave_reference_comment(issuable:, from_project_name:)
- project = Project.find_by(name: from_project_name)
-
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "##{issuable.to_reference(project)}"
- click_button 'Comment'
- end
- end
-
- def visible_note(issuable:, from_project_name:, user_name:)
- project = Project.find_by(name: from_project_name)
-
- expect(page).to have_content(user_name)
- expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
- end
-
- def expect_sidebar_content(content)
- page.within '.issuable-sidebar' do
- expect(page).to have_content content
- end
- end
-end
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
deleted file mode 100644
index 9d522936fb6..00000000000
--- a/features/steps/shared/markdown.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module SharedMarkdown
- include Spinach::DSL
-
- def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
- node = find("#{parent} h#{level} a#user-content-#{id}")
- expect(node[:href]).to end_with "##{id}"
-
- # Work around a weird Capybara behavior where calling `parent` on a node
- # returns the whole document, not the node's actual parent element
- expect(find(:xpath, "#{node.path}/..").text).to eq text
- end
-
- step 'I should not see the Markdown preview' do
- expect(find('.gfm-form .js-md-preview')).not_to be_visible
- end
-
- step 'I haven\'t written any description text' do
- find('.gfm-form').fill_in 'Description', with: ''
- end
-end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
deleted file mode 100644
index bf1b88c60d7..00000000000
--- a/features/steps/shared/note.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module SharedNote
- include Spinach::DSL
- include WaitForRequests
-
- after do
- wait_for_requests if javascript_test?
- end
-
- step 'I haven\'t written any comment text' do
- page.within(".js-main-target-form") do
- fill_in "note[note]", with: ""
- end
- end
-
- step 'The comment preview tab should say there is nothing to do' do
- page.within(".js-main-target-form") do
- find('.js-md-preview-button').click
- expect(find('.js-md-preview')).to have_content('Nothing to preview.')
- end
- end
-
- step 'I should see no notes at all' do
- expect(page).not_to have_css('.note')
- end
-end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
deleted file mode 100644
index d16c127f6e6..00000000000
--- a/features/steps/shared/paths.rb
+++ /dev/null
@@ -1,438 +0,0 @@
-module SharedPaths
- include Spinach::DSL
- include RepoHelpers
- include DashboardHelper
- include WaitForRequests
-
- step 'I visit new project page' do
- visit new_project_path
- end
-
- step 'I visit login page' do
- visit new_user_session_path
- end
-
- # ----------------------------------------
- # User
- # ----------------------------------------
-
- step 'I visit user "John Doe" page' do
- visit user_path("john_doe")
- end
-
- # ----------------------------------------
- # Group
- # ----------------------------------------
-
- step 'I visit group "Owned" page' do
- visit group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" activity page' do
- visit activity_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" issues page' do
- visit issues_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" merge requests page' do
- visit merge_requests_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" milestones page' do
- visit group_milestones_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" members page' do
- visit group_group_members_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" settings page' do
- visit edit_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Owned" projects page' do
- visit projects_group_path(Group.find_by(name: "Owned"))
- end
-
- step 'I visit group "Guest" page' do
- visit group_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" issues page' do
- visit issues_group_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" merge requests page' do
- visit merge_requests_group_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" members page' do
- visit group_group_members_path(Group.find_by(name: "Guest"))
- end
-
- step 'I visit group "Guest" settings page' do
- visit edit_group_path(Group.find_by(name: "Guest"))
- end
-
- # ----------------------------------------
- # Dashboard
- # ----------------------------------------
-
- step 'I visit dashboard page' do
- visit dashboard_projects_path
- end
-
- step 'I visit dashboard activity page' do
- visit activity_dashboard_path
- end
-
- step 'I visit dashboard projects page' do
- visit projects_dashboard_path
- end
-
- step 'I visit dashboard issues page' do
- visit assigned_issues_dashboard_path
- end
-
- step 'I visit dashboard search page' do
- visit search_path
- end
-
- step 'I visit dashboard help page' do
- visit help_path
- end
-
- step 'I visit dashboard groups page' do
- visit dashboard_groups_path
- end
-
- step 'I should be redirected to the dashboard groups page' do
- expect(current_path).to eq dashboard_groups_path
- end
-
- step 'I visit dashboard starred projects page' do
- visit starred_dashboard_projects_path
- end
-
- # ----------------------------------------
- # Profile
- # ----------------------------------------
-
- step 'I visit profile page' do
- visit profile_path
- end
-
- step 'I visit profile applications page' do
- visit applications_profile_path
- end
-
- step 'I visit profile password page' do
- visit edit_profile_password_path
- end
-
- step 'I visit profile account page' do
- visit profile_account_path
- end
-
- step 'I visit profile SSH keys page' do
- visit profile_keys_path
- end
-
- step 'I visit profile preferences page' do
- visit profile_preferences_path
- end
-
- step 'I visit Authentication log page' do
- visit audit_log_profile_path
- end
-
- # ----------------------------------------
- # Admin
- # ----------------------------------------
-
- step 'I visit admin page' do
- visit admin_root_path
- end
-
- step 'I visit abuse reports page' do
- visit admin_abuse_reports_path
- end
-
- step 'I visit admin projects page' do
- visit admin_projects_path
- end
-
- step 'I visit admin users page' do
- visit admin_users_path
- end
-
- step 'I visit admin logs page' do
- visit admin_logs_path
- end
-
- step 'I visit admin messages page' do
- visit admin_broadcast_messages_path
- end
-
- step 'I visit admin hooks page' do
- visit admin_hooks_path
- end
-
- step 'I visit admin Resque page' do
- visit admin_background_jobs_path
- end
-
- step 'I visit admin teams page' do
- visit admin_teams_path
- end
-
- step 'I visit spam logs page' do
- visit admin_spam_logs_path
- end
-
- # ----------------------------------------
- # Generic Project
- # ----------------------------------------
-
- step "I visit my project's settings page" do
- visit edit_project_path(@project)
- end
-
- step "I visit my project's files page" do
- visit project_tree_path(@project, root_ref)
- end
-
- step 'I visit a binary file in the repo' do
- visit project_blob_path(@project,
- File.join(root_ref, 'files/images/logo-black.png'))
- end
-
- step "I visit my project's commits page" do
- visit project_commits_path(@project, root_ref, { limit: 5 })
- end
-
- step "I visit my project's commits page for a specific path" do
- visit project_commits_path(@project, root_ref + "/files/ruby/regex.rb", { limit: 5 })
- end
-
- step 'I visit my project\'s commits stats page' do
- visit stats_project_repository_path(@project)
- end
-
- step "I visit my project's graph page" do
- # Stub Graph max_size to speed up test (10 commits vs. 650)
- Network::Graph.stub(max_count: 10)
-
- visit project_network_path(@project, root_ref)
- end
-
- step "I visit my project's issues page" do
- visit project_issues_path(@project)
- end
-
- step "I visit my project's merge requests page" do
- visit project_merge_requests_path(@project)
- end
-
- step "I visit my project's members page" do
- visit project_project_members_path(@project)
- end
-
- step "I visit my project's wiki page" do
- visit project_wiki_path(@project, :home)
- end
-
- step 'I visit project hooks page' do
- visit project_settings_integrations_path(@project)
- end
-
- step 'I visit project deploy keys page' do
- visit project_deploy_keys_path(@project)
- end
-
- step 'I visit project find file page' do
- visit project_find_file_path(@project, root_ref)
- end
-
- # ----------------------------------------
- # "Shop" Project
- # ----------------------------------------
-
- step 'I visit project "Shop" page' do
- visit project_path(project)
- end
-
- step 'I visit project "Forked Shop" merge requests page' do
- visit project_merge_requests_path(@forked_project)
- end
-
- step 'I visit edit project "Shop" page' do
- visit edit_project_path(project)
- end
-
- step 'I visit compare refs page' do
- visit project_compare_index_path(@project)
- end
-
- step 'I visit project commits page' do
- visit project_commits_path(@project, root_ref, { limit: 5 })
- end
-
- step 'I visit project commits page for stable branch' do
- visit project_commits_path(@project, 'stable', { limit: 5 })
- end
-
- step 'I visit blob file from repo' do
- visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path))
- end
-
- step 'I visit ".gitignore" file in repo' do
- visit project_blob_path(@project, File.join(root_ref, '.gitignore'))
- end
-
- step 'I am on the new file page' do
- expect(current_path).to eq(project_create_blob_path(@project, root_ref))
- end
-
- step 'I am on the ".gitignore" edit file page' do
- expect(current_path).to eq(
- project_edit_blob_path(@project, File.join(root_ref, '.gitignore')))
- end
-
- step 'I visit project source page for "6d39438"' do
- visit project_tree_path(@project, "6d39438")
- end
-
- step 'I visit project source page for' \
- ' "6d394385cf567f80a8fd85055db1ab4c5295806f"' do
- visit project_tree_path(@project,
- '6d394385cf567f80a8fd85055db1ab4c5295806f')
- end
-
- step 'I visit project tags page' do
- visit project_tags_path(@project)
- end
-
- step 'I visit project commit page' do
- visit project_commit_path(@project, sample_commit.id)
- end
-
- step 'I visit issue page "Release 0.4"' do
- issue = Issue.find_by(title: "Release 0.4")
- visit project_issue_path(issue.project, issue)
- end
-
- step 'I visit project "Forum" labels page' do
- project = Project.find_by(name: 'Forum')
- visit project_labels_path(project)
- end
-
- step 'I visit project "Shop" new label page' do
- project = Project.find_by(name: 'Shop')
- visit new_project_label_path(project)
- end
-
- step 'I visit project "Forum" new label page' do
- project = Project.find_by(name: 'Forum')
- visit new_project_label_path(project)
- end
-
- step 'I visit merge request page "Bug NS-04"' do
- visit merge_request_path("Bug NS-04")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug NS-05"' do
- visit merge_request_path("Bug NS-05")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug NS-07"' do
- visit merge_request_path("Bug NS-07")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug NS-08"' do
- visit merge_request_path("Bug NS-08")
- wait_for_requests
- end
-
- step 'I visit merge request page "Bug CO-01"' do
- mr = MergeRequest.find_by(title: "Bug CO-01")
- visit project_merge_request_path(mr.target_project, mr)
- wait_for_requests
- end
-
- step 'I visit forked project "Shop" merge requests page' do
- visit project_merge_requests_path(project)
- end
-
- step 'I visit project "Shop" team page' do
- visit project_project_members_path(project)
- end
-
- step 'I visit project wiki page' do
- visit project_wiki_path(@project, :home)
- end
-
- # ----------------------------------------
- # Visibility Projects
- # ----------------------------------------
-
- step 'I visit project "Community" source page' do
- project = Project.find_by(name: 'Community')
- visit project_tree_path(project, root_ref)
- end
-
- step 'I visit project "Internal" page' do
- project = Project.find_by(name: "Internal")
- visit project_path(project)
- end
-
- step 'I visit project "Enterprise" page' do
- project = Project.find_by(name: "Enterprise")
- visit project_path(project)
- end
-
- # ----------------------------------------
- # Empty Projects
- # ----------------------------------------
-
- step "I should not see command line instructions" do
- expect(page).not_to have_css('.empty_wrapper')
- end
-
- # ----------------------------------------
- # Public Projects
- # ----------------------------------------
- step 'I visit the public groups area' do
- visit explore_groups_path
- end
-
- # ----------------------------------------
- # Snippets
- # ----------------------------------------
-
- step 'I visit project "Shop" snippets page' do
- visit project_snippets_path(project)
- end
-
- step 'I visit snippets page' do
- visit explore_snippets_path
- end
-
- def root_ref
- @project.repository.root_ref
- end
-
- def project
- Project.find_by!(name: 'Shop')
- end
-
- def merge_request_path(title)
- mr = MergeRequest.find_by(title: title)
- project_merge_request_path(mr.target_project, mr)
- end
-end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
deleted file mode 100644
index a1945cf5f3d..00000000000
--- a/features/steps/shared/project.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-module SharedProject
- include Spinach::DSL
-
- # Create a project without caring about what it's called
- step "I own a project" do
- @project = create(:project, :repository, namespace: @user.namespace)
- @project.add_master(@user)
- end
-
- step "I own a project in some group namespace" do
- @group = create(:group, name: 'some group')
- @project = create(:project, namespace: @group)
- @project.add_master(@user)
- end
-
- # Create a specific project called "Shop"
- step 'I own project "Shop"' do
- @project = Project.find_by(name: "Shop")
- @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace)
- @project.add_master(@user)
- end
-
- def current_project
- @project ||= Project.first
- end
-
- # ----------------------------------------
- # Visibility of archived project
- # ----------------------------------------
-
- step 'I should not see project "Archive"' do
- project = Project.find_by(name: "Archive")
- expect(page).not_to have_content project.full_name
- end
-
- step 'I should see project "Archive"' do
- project = Project.find_by(name: "Archive")
- expect(page).to have_content project.full_name
- end
-
- # ----------------------------------------
- # Visibility level
- # ----------------------------------------
-
- step 'private project "Enterprise"' do
- create(:project, :private, :repository, name: 'Enterprise')
- end
-
- step 'I should see project "Enterprise"' do
- expect(page).to have_content "Enterprise"
- end
-
- step 'I should not see project "Enterprise"' do
- expect(page).not_to have_content "Enterprise"
- end
-
- step 'internal project "Internal"' do
- create(:project, :internal, :repository, name: 'Internal')
- end
-
- step 'I should see project "Internal"' do
- page.within '.js-projects-list-holder' do
- expect(page).to have_content "Internal"
- end
- end
-
- step 'I should not see project "Internal"' do
- page.within '.js-projects-list-holder' do
- expect(page).not_to have_content "Internal"
- end
- end
-
- step 'public project "Community"' do
- create(:project, :public, :repository, name: 'Community')
- end
-
- step 'I should see project "Community"' do
- expect(page).to have_content "Community"
- end
-
- step 'I should not see project "Community"' do
- expect(page).not_to have_content "Community"
- end
-
- step '"John Doe" owns private project "Enterprise"' do
- user_owns_project(
- user_name: 'John Doe',
- project_name: 'Enterprise'
- )
- end
-
- step '"Mary Jane" owns private project "Enterprise"' do
- user_owns_project(
- user_name: 'Mary Jane',
- project_name: 'Enterprise'
- )
- end
-
- step '"John Doe" owns internal project "Internal"' do
- user_owns_project(
- user_name: 'John Doe',
- project_name: 'Internal',
- visibility: :internal
- )
- end
-
- step '"John Doe" owns public project "Community"' do
- user_owns_project(
- user_name: 'John Doe',
- project_name: 'Community',
- visibility: :public
- )
- end
-
- step 'public empty project "Empty Public Project"' do
- create :project_empty_repo, :public, name: "Empty Public Project"
- end
-
- step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
- project = Project.find_by(name: "Shop")
- create(:label, project: project, title: 'bug')
- create(:label, project: project, title: 'feature')
- create(:label, project: project, title: 'enhancement')
- end
-
- def user_owns_project(user_name:, project_name:, visibility: :private)
- user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
- project = Project.find_by(name: project_name)
- project ||= create(:project, visibility, name: project_name, namespace: user.namespace)
- project.add_master(user)
- end
-end
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
deleted file mode 100644
index 5a516ee33bc..00000000000
--- a/features/steps/shared/project_tab.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-require_relative 'active_tab'
-
-module SharedProjectTab
- include Spinach::DSL
- include SharedActiveTab
-
- step 'the active main tab should be Project' do
- ensure_active_main_tab('Overview')
- end
-
- step 'the active main tab should be Repository' do
- ensure_active_main_tab('Repository')
- end
-
- step 'the active main tab should be Issues' do
- ensure_active_main_tab('Issues')
- end
-
- step 'the active sub tab should be Members' do
- ensure_active_sub_tab('Members')
- end
-
- step 'the active main tab should be Merge Requests' do
- ensure_active_main_tab('Merge Requests')
- end
-
- step 'the active main tab should be Snippets' do
- ensure_active_main_tab('Snippets')
- end
-
- step 'the active main tab should be Wiki' do
- ensure_active_main_tab('Wiki')
- end
-
- step 'the active main tab should be Members' do
- ensure_active_main_tab('Members')
- end
-
- step 'the active main tab should be Settings' do
- ensure_active_main_tab('Settings')
- end
-
- step 'the active sub tab should be Graph' do
- ensure_active_sub_tab('Graph')
- end
-
- step 'the active sub tab should be Files' do
- ensure_active_sub_tab('Files')
- end
-
- step 'the active sub tab should be Commits' do
- ensure_active_sub_tab('Commits')
- end
-
- step 'the active sub tab should be Home' do
- ensure_active_sub_tab('Details')
- end
-
- step 'the active sub tab should be Activity' do
- ensure_active_sub_tab('Activity')
- end
-
- step 'the active sub tab should be Charts' do
- ensure_active_sub_tab('Charts')
- end
-end
diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb
deleted file mode 100644
index a75a8474d26..00000000000
--- a/features/steps/shared/shortcuts.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module SharedShortcuts
- include Spinach::DSL
-
- step 'I press "g" and "p"' do
- find('body').native.send_key('g')
- find('body').native.send_key('p')
- end
-
- step 'I press "g" and "i"' do
- find('body').native.send_key('g')
- find('body').native.send_key('i')
- end
-
- step 'I press "g" and "m"' do
- find('body').native.send_key('g')
- find('body').native.send_key('m')
- end
-end
diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb
deleted file mode 100644
index 07fff16e867..00000000000
--- a/features/steps/shared/sidebar_active_tab.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module SharedSidebarActiveTab
- include Spinach::DSL
-
- step 'no other main tabs should be active' do
- expect(page).to have_selector('.nav-sidebar li.active', count: 1)
- end
-
- def ensure_active_main_tab(content)
- expect(find('.nav-sidebar li.active')).to have_content(content)
- end
-
- step 'the active main tab should be Home' do
- ensure_active_main_tab('Projects')
- end
-
- step 'the active main tab should be Groups' do
- ensure_active_main_tab('Groups')
- end
-
- step 'the active main tab should be Projects' do
- ensure_active_main_tab('Projects')
- end
-
- step 'the active main tab should be Issues' do
- ensure_active_main_tab('Issues')
- end
-
- step 'the active main tab should be Merge Requests' do
- ensure_active_main_tab('Merge Requests')
- end
-end
diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb
deleted file mode 100644
index 9cadc91769d..00000000000
--- a/features/steps/shared/user.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-module SharedUser
- include Spinach::DSL
-
- step 'User "John Doe" exists' do
- user_exists("John Doe", { username: "john_doe" })
- end
-
- step 'User "Mary Jane" exists' do
- user_exists("Mary Jane", { username: "mary_jane" })
- end
-
- step 'gitlab user "Mike"' do
- create(:user, name: "Mike")
- end
-
- protected
-
- def user_exists(name, options = {})
- User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options))
- end
-
- step 'I have no ssh keys' do
- @user.keys.delete_all
- end
-
- step 'I click on "Personal projects" tab' do
- page.within '.nav-links' do
- click_link 'Personal projects'
- end
-
- expect(page).to have_css('.tab-content #projects.active')
- end
-
- step 'I click on "Contributed projects" tab' do
- page.within '.nav-links' do
- click_link 'Contributed projects'
- end
-
- expect(page).to have_css('.tab-content #contributed.active')
- end
-end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
deleted file mode 100644
index 8879c9ab650..00000000000
--- a/features/support/capybara.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'capybara-screenshot/spinach'
-
-# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-
-Capybara.register_driver :chrome do |app|
- capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
- # This enables access to logs with `page.driver.manage.get_log(:browser)`
- loggingPrefs: {
- browser: "ALL",
- client: "ALL",
- driver: "ALL",
- server: "ALL"
- }
- )
-
- options = Selenium::WebDriver::Chrome::Options.new
- options.add_argument("window-size=1240,1400")
-
- # Chrome won't work properly in a Docker container in sandbox mode
- options.add_argument("no-sandbox")
-
- # Run headless by default unless CHROME_HEADLESS specified
- options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
-
- # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
- options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER']
-
- Capybara::Selenium::Driver.new(
- app,
- browser: :chrome,
- desired_capabilities: capabilities,
- options: options
- )
-end
-
-Capybara.javascript_driver = :chrome
-Capybara.default_max_wait_time = timeout
-Capybara.ignore_hidden_elements = false
-
-# Keep only the screenshots generated from the last failing test suite
-Capybara::Screenshot.prune_strategy = :keep_last_run
-# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326
-Capybara::Screenshot.register_driver(:chrome) do |driver, path|
- driver.browser.save_screenshot(path)
-end
-
-Spinach.hooks.before_run do
- TestEnv.eager_load_driver_server
-end
diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb
deleted file mode 100644
index 31c922d23c3..00000000000
--- a/features/support/db_cleaner.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'database_cleaner'
-
-DatabaseCleaner[:active_record].strategy = :deletion
-
-Spinach.hooks.before_scenario do
- DatabaseCleaner.start
-end
-
-Spinach.hooks.after_scenario do
- DatabaseCleaner.clean
-end
diff --git a/features/support/env.rb b/features/support/env.rb
deleted file mode 100644
index 15211995918..00000000000
--- a/features/support/env.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require './spec/simplecov_env'
-SimpleCovEnv.start!
-
-ENV['RAILS_ENV'] = 'test'
-require './config/environment'
-require 'rspec/expectations'
-
-if ENV['CI']
- require 'knapsack'
- Knapsack::Adapters::SpinachAdapter.bind
-end
-
-WebMock.enable!
-
-%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper webmock).each do |f|
- require Rails.root.join('spec', 'support', f)
-end
-
-Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
-
-Spinach.hooks.before_run do
- include RSpec::Mocks::ExampleMethods
- include ActiveJob::TestHelper
- include FactoryBot::Syntax::Methods
- include GitlabRoutingHelper
-
- RSpec::Mocks.setup
- TestEnv.init(mailer: false)
-
- # skip pre-receive hook check so we can use
- # web editor and merge
- TestEnv.disable_pre_receive
-end
-
-Spinach.hooks.after_scenario do |scenario_data, step_definitions|
- if scenario_data.tags.include?('javascript')
- include WaitForRequests
- block_and_wait_for_requests_complete
- end
-end
-
-module StdoutReporterWithScenarioLocation
- # Override the standard reporter to show filename and line number next to each
- # scenario for easy, focused re-runs
- def before_scenario_run(scenario, step_definitions = nil)
- @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables
- name = scenario.name
-
- # This number has no significance, it's just to line things up
- max_length = @max_step_name_length + 19 # rubocop:disable Gitlab/ModuleWithInstanceVariables
- out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \
- " # #{scenario.feature.filename}:#{scenario.line}"
- end
-end
-
-Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation)
diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb
deleted file mode 100644
index 3cd5f4ce497..00000000000
--- a/features/support/gitaly.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-Spinach.hooks.before_scenario do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
-end
diff --git a/features/support/login_helpers.rb b/features/support/login_helpers.rb
deleted file mode 100644
index 540ff25a4f2..00000000000
--- a/features/support/login_helpers.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module LoginHelpers
- # After inclusion, IntegrationHelpers calls these two methods that aren't
- # supported by Spinach, so we perform the end results ourselves
- class << self
- def setup(*args)
- Spinach.hooks.before_scenario do
- Warden.test_mode!
- end
- end
-
- def teardown(*args)
- Spinach.hooks.after_scenario do
- Warden.test_reset!
- end
- end
- end
-
- include Devise::Test::IntegrationHelpers
-end
diff --git a/features/support/rerun.rb b/features/support/rerun.rb
deleted file mode 100644
index 60b78f9d050..00000000000
--- a/features/support/rerun.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# The spinach-rerun-reporter doesn't define the on_undefined_step
-# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb
-require 'spinach-rerun-reporter'
-
-module Spinach
- class Reporter
- class Rerun
- def on_undefined_step(step_data, failure, step_definitions = nil)
- super step_data, failure, step_definitions
-
- # save feature file and scenario line
- @rerun << "#{current_feature.filename}:#{current_scenario.line}"
- end
- end
- end
-end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 073471b4c4d..de20b2b8e67 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -8,14 +8,15 @@ module API
PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
- use GrapeLogging::Middleware::RequestLogger,
- logger: Logger.new(LOG_FILENAME),
- formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
- include: [
- GrapeLogging::Loggers::FilterParameters.new,
- GrapeLogging::Loggers::ClientEnv.new,
- Gitlab::GrapeLogging::Loggers::UserLogger.new
- ]
+ insert_before Grape::Middleware::Error,
+ GrapeLogging::Middleware::RequestLogger,
+ logger: Logger.new(LOG_FILENAME),
+ formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
+ include: [
+ GrapeLogging::Loggers::FilterParameters.new,
+ GrapeLogging::Loggers::ClientEnv.new,
+ Gitlab::GrapeLogging::Loggers::UserLogger.new
+ ]
allow_access_with_scope :api
prefix :api
@@ -139,6 +140,7 @@ module API
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
+ mount ::API::Markdown
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
@@ -154,6 +156,7 @@ module API
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectMilestones
+ mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::Repositories
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index c2113551207..c17089759de 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -45,7 +45,9 @@ module API
user = find_user_from_sources
return unless user
- forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ unless api_access_allowed?(user)
+ forbidden!(api_access_denied_message(user))
+ end
user
end
@@ -72,6 +74,14 @@ module API
end
end
end
+
+ def api_access_allowed?(user)
+ Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ end
+
+ def api_access_denied_message(user)
+ Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
+ end
end
module ClassMethods
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 7975f35ab1e..13c34e3473a 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -5,11 +5,12 @@ module API
before { authenticate! }
- NOTEABLE_TYPES = [Issue, Snippet].freeze
+ NOTEABLE_TYPES = [Issue, Snippet, MergeRequest, Commit].freeze
NOTEABLE_TYPES.each do |noteable_type|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
+ noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
@@ -19,14 +20,12 @@ module API
success Entities::Discussion
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
use :pagination
end
- get ":id/#{noteables_str}/:noteable_id/discussions" do
+ get ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- break not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
-
notes = noteable.notes
.inc_relations_for_view
.includes(:noteable)
@@ -43,13 +42,13 @@ module API
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
- get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id" do
+ get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
- if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if notes.empty?
break not_found!("Discussion")
end
@@ -62,19 +61,36 @@ module API
success Entities::Discussion
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
+ optional :position, type: Hash do
+ requires :base_sha, type: String, desc: 'Base commit SHA in the source branch'
+ requires :start_sha, type: String, desc: 'SHA referencing commit in target branch'
+ requires :head_sha, type: String, desc: 'SHA referencing HEAD of this merge request'
+ requires :position_type, type: String, desc: 'Type of the position reference', values: %w(text image)
+ optional :new_path, type: String, desc: 'File path after change'
+ optional :new_line, type: Integer, desc: 'Line number after change'
+ optional :old_path, type: String, desc: 'File path before change'
+ optional :old_line, type: Integer, desc: 'Line number before change'
+ optional :width, type: Integer, desc: 'Width of the image'
+ optional :height, type: Integer, desc: 'Height of the image'
+ optional :x, type: Integer, desc: 'X coordinate in the image'
+ optional :y, type: Integer, desc: 'Y coordinate in the image'
+ end
end
- post ":id/#{noteables_str}/:noteable_id/discussions" do
+ post ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+ type = params[:position] ? 'DiffNote' : 'DiscussionNote'
+ id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id
opts = {
note: params[:body],
created_at: params[:created_at],
- type: 'DiscussionNote',
+ type: type,
noteable_type: noteables_str.classify,
- noteable_id: noteable.id
+ position: params[:position],
+ id_key => noteable.id
}
note = create_note(noteable, opts)
@@ -91,13 +107,13 @@ module API
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
end
- get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
+ get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
- if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if notes.empty?
break not_found!("Notes")
end
@@ -108,12 +124,12 @@ module API
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
end
- post ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
+ post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
@@ -139,11 +155,11 @@ module API
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
- get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
get_note(noteable, params[:note_id])
@@ -153,30 +169,52 @@ module API
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
- requires :body, type: String, desc: 'The content of a note'
+ optional :body, type: String, desc: 'The content of a note'
+ optional :resolved, type: Boolean, desc: 'Mark note resolved/unresolved'
+ exactly_one_of :body, :resolved
end
- put ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- update_note(noteable, params[:note_id])
+ if params[:resolved].nil?
+ update_note(noteable, params[:note_id])
+ else
+ resolve_note(noteable, params[:note_id], params[:resolved])
+ end
end
desc "Delete a comment in a #{noteable_type.to_s.downcase} discussion" do
success Entities::Note
end
params do
- requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
- delete ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
delete_note(noteable, params[:note_id])
end
+
+ if Noteable::RESOLVABLE_TYPES.include?(noteable_type.to_s)
+ desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Discussion
+ end
+ params do
+ requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved'
+ end
+ put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+
+ resolve_discussion(noteable, params[:discussion_id], params[:resolved])
+ end
+ end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 8aad320e376..174c5af91d5 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -125,7 +125,7 @@ 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
+ expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url
expose :avatar_url do |project, options|
project.avatar_url(only_path: false)
end
@@ -136,6 +136,7 @@ module API
def self.preload_relation(projects_relation, options = {})
projects_relation.preload(:project_feature, :route)
+ .preload(:import_state)
.preload(namespace: [:route, :owner],
tags: :taggings)
end
@@ -149,11 +150,11 @@ module API
expose_url(api_v4_projects_path(id: project.id))
end
- expose :issues, if: -> (*args) { issues_available?(*args) } do |project|
+ expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project|
expose_url(api_v4_projects_issues_path(id: project.id))
end
- expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project|
+ expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project|
expose_url(api_v4_projects_merge_requests_path(id: project.id))
end
@@ -242,13 +243,18 @@ module API
expose :requested_at
end
- class Group < Grape::Entity
- expose :id, :name, :path, :description, :visibility
+ class BasicGroupDetails < Grape::Entity
+ expose :id
+ expose :web_url
+ expose :name
+ end
+
+ class Group < BasicGroupDetails
+ expose :path, :description, :visibility
expose :lfs_enabled?, as: :lfs_enabled
expose :avatar_url do |group, options|
group.avatar_url(only_path: false)
end
- expose :web_url
expose :request_access_enabled
expose :full_name, :full_path
@@ -286,6 +292,10 @@ module API
end
end
+ class DiffRefs < Grape::Entity
+ expose :base_sha, :head_sha, :start_sha
+ end
+
class Commit < Grape::Entity
expose :id, :short_id, :title, :created_at
expose :parent_ids
@@ -601,6 +611,8 @@ module API
merge_request.metrics&.pipeline
end
+ expose :diff_refs, using: Entities::DiffRefs
+
def build_available?(options)
options[:project]&.feature_available?(:builds, options[:current_user])
end
@@ -642,6 +654,11 @@ module API
expose :id, :key, :created_at
end
+ class DiffPosition < Grape::Entity
+ expose :base_sha, :start_sha, :head_sha, :old_path, :new_path,
+ :position_type
+ end
+
class Note < Grape::Entity
# Only Issue and MergeRequest have iid
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
@@ -655,6 +672,14 @@ module API
expose :system?, as: :system
expose :noteable_id, :noteable_type
+ expose :position, if: ->(note, options) { note.diff_note? } do |note|
+ note.position.to_h
+ end
+
+ expose :resolvable?, as: :resolvable
+ expose :resolved?, as: :resolved, if: ->(note, options) { note.resolvable? }
+ expose :resolved_by, using: Entities::UserBasic, if: ->(note, options) { note.resolvable? }
+
# Avoid N+1 queries as much as possible
expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
end
@@ -942,6 +967,7 @@ module API
class Runner < Grape::Entity
expose :id
expose :description
+ expose :ip_address
expose :active
expose :is_shared
expose :name
@@ -965,6 +991,13 @@ module API
options[:current_user].authorized_projects.where(id: runner.projects)
end
end
+ expose :groups, with: Entities::BasicGroupDetails do |runner, options|
+ if options[:current_user].admin?
+ runner.groups
+ else
+ options[:current_user].authorized_groups.where(id: runner.groups)
+ end
+ end
end
class RunnerRegistrationDetails < Grape::Entity
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 4a4df1b8b9e..03b6b30a0d8 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -37,13 +37,11 @@ module API
use :pagination
end
- def find_groups(params)
- find_params = {
- all_available: params[:all_available],
- custom_attributes: params[:custom_attributes],
- owned: params[:owned]
- }
- find_params[:parent] = find_group!(params[:id]) if params[:id]
+ def find_groups(params, parent_id = nil)
+ find_params = params.slice(:all_available, :custom_attributes, :owned)
+ find_params[:parent] = find_group!(parent_id) if parent_id
+ find_params[:all_available] =
+ find_params.fetch(:all_available, current_user&.full_private_access?)
groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
@@ -85,7 +83,7 @@ module API
use :with_custom_attributes
end
get do
- groups = find_groups(params)
+ groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups params, groups
end
@@ -167,9 +165,12 @@ module API
group = find_group!(params[:id])
authorize! :admin_group, group
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/46285')
destroy_conditionally!(group) do |group|
- ::Groups::DestroyService.new(group, current_user).execute
+ ::Groups::DestroyService.new(group, current_user).async_execute
end
+
+ accepted!
end
desc 'Get a list of projects in this group.' do
@@ -213,7 +214,7 @@ module API
use :with_custom_attributes
end
get ":id/subgroups" do
- groups = find_groups(params)
+ groups = find_groups(declared_params(include_missing: false), params[:id])
present_groups params, groups
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index b8657cd7ee4..2ed331d4fd2 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -171,6 +171,10 @@ module API
MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
+ def find_project_commit(id)
+ user_project.commit_by(oid: id)
+ end
+
def find_project_snippet(id)
finder_params = { project: user_project }
SnippetsFinder.new(current_user, finder_params).find(id)
diff --git a/lib/api/helpers/custom_attributes.rb b/lib/api/helpers/custom_attributes.rb
index 70e4eda95f8..10d652e33f5 100644
--- a/lib/api/helpers/custom_attributes.rb
+++ b/lib/api/helpers/custom_attributes.rb
@@ -7,6 +7,9 @@ module API
helpers do
params :with_custom_attributes do
optional :with_custom_attributes, type: Boolean, default: false, desc: 'Include custom attributes in the response'
+
+ optional :custom_attributes, type: Hash,
+ desc: 'Filter with custom attributes'
end
def with_custom_attributes(collection_or_resource, options = {})
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index abe3d353984..83151be82ad 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -89,12 +89,6 @@ module API
end
end
- # Return the repository full path so that gitlab-shell has it when
- # handling ssh commands
- def repository_path
- repository.path_to_repo
- end
-
# Return the Gitaly Address if it is enabled
def gitaly_payload(action)
return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action)
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index cd91df1ecd8..b4bfb677d72 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -21,6 +21,23 @@ module API
end
end
+ def resolve_note(noteable, note_id, resolved)
+ note = noteable.notes.find(note_id)
+
+ authorize! :resolve_note, note
+
+ bad_request!("Note is not resolvable") unless note.resolvable?
+
+ if resolved
+ parent = noteable_parent(noteable)
+ ::Notes::ResolveService.new(parent, current_user).execute(note)
+ else
+ note.unresolve!
+ end
+
+ present note, with: Entities::Note
+ end
+
def delete_note(noteable, note_id)
note = noteable.notes.find(note_id)
@@ -35,7 +52,7 @@ module API
def get_note(noteable, note_id)
note = noteable.notes.with_metadata.find(params[:note_id])
- can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
+ can_read_note = !note.cross_reference_not_visible_for?(current_user)
if can_read_note
present note, with: Entities::Note
@@ -49,7 +66,20 @@ module API
end
def find_noteable(parent, noteables_str, noteable_id)
- public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
+ noteable = public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
+
+ readable =
+ if noteable.is_a?(Commit)
+ # for commits there is not :read_commit policy, check if user
+ # has :read_note permission on the commit's project
+ can?(current_user, :read_note, user_project)
+ else
+ can?(current_user, noteable_read_ability_name(noteable), noteable)
+ end
+
+ return not_found!(noteables_str) unless readable
+
+ noteable
end
def noteable_parent(noteable)
@@ -57,20 +87,34 @@ module API
end
def create_note(noteable, opts)
- noteables_str = noteable.model_name.to_s.underscore.pluralize
-
- return not_found!(noteables_str) unless can?(current_user, noteable_read_ability_name(noteable), noteable)
-
- authorize! :create_note, noteable
+ policy_object = noteable.is_a?(Commit) ? user_project : noteable
+ authorize!(:create_note, policy_object)
parent = noteable_parent(noteable)
+
if opts[:created_at]
- opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user
+ opts.delete(:created_at) unless
+ current_user.admin? || parent.owned_by?(current_user)
end
project = parent if parent.is_a?(Project)
::Notes::CreateService.new(project, current_user, opts).execute
end
+
+ def resolve_discussion(noteable, discussion_id, resolved)
+ discussion = noteable.find_discussion(discussion_id)
+
+ forbidden! unless discussion.can_resolve?(current_user)
+
+ if resolved
+ parent = noteable_parent(noteable)
+ ::Discussions::ResolveService.new(parent, current_user, merge_request: noteable).execute(discussion)
+ else
+ discussion.unresolve!
+ end
+
+ present discussion, with: Entities::Discussion
+ end
end
end
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 09805049169..3308212216e 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -2,67 +2,240 @@ module API
module Helpers
module Pagination
def paginate(relation)
- relation = add_default_order(relation)
+ strategy = if params[:pagination] == 'keyset' && Feature.enabled?('api_keyset_pagination')
+ KeysetPaginationStrategy
+ else
+ DefaultPaginationStrategy
+ end
- relation.page(params[:page]).per(params[:per_page]).tap do |data|
- add_pagination_headers(data)
- end
+ strategy.new(self).paginate(relation)
end
- private
+ class KeysetPaginationInfo
+ attr_reader :relation, :request_context
- def add_pagination_headers(paginated_data)
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
+ def initialize(relation, request_context)
+ # This is because it's rather complex to support multiple values with possibly different sort directions
+ # (and we don't need this in the API)
+ if relation.order_values.size > 1
+ raise "Pagination only supports ordering by a single column." \
+ "The following columns were given: #{relation.order_values.map { |v| v.expr.name }}"
+ end
- return if data_without_counts?(paginated_data)
+ @relation = relation
+ @request_context = request_context
+ end
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', total_pages(paginated_data).to_s
- end
+ def fields
+ keys.zip(values).reject { |_, v| v.nil? }.to_h
+ end
- def pagination_links(paginated_data)
- request_url = request.url.split('?').first
- request_params = params.clone
- request_params[:per_page] = paginated_data.limit_value
+ def column_for_order_by(relation)
+ relation.order_values.first&.expr&.name
+ end
- links = []
+ # Sort direction (`:asc` or `:desc`)
+ def sort
+ @sort ||= if order_by_primary_key?
+ # Default order is by id DESC
+ :desc
+ else
+ # API defaults to DESC order if param `sort` not present
+ request_context.params[:sort]&.to_sym || :desc
+ end
+ end
- request_params[:page] = paginated_data.prev_page
- links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page]
+ # Do we only sort by primary key?
+ def order_by_primary_key?
+ keys.size == 1 && keys.first == primary_key
+ end
- request_params[:page] = paginated_data.next_page
- links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page]
+ def primary_key
+ relation.model.primary_key.to_sym
+ end
- request_params[:page] = 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+ def sort_ascending?
+ sort == :asc
+ end
- unless data_without_counts?(paginated_data)
- request_params[:page] = total_pages(paginated_data)
- links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+ # Build hash of request parameters for a given record (relevant to pagination)
+ def params_for(record)
+ return {} unless record
+
+ keys.each_with_object({}) do |key, h|
+ h["ks_prev_#{key}".to_sym] = record.attributes[key.to_s]
+ end
end
- links.join(', ')
- end
+ private
+
+ # All values present in request parameters that correspond to #keys.
+ def values
+ @values ||= keys.map do |key|
+ request_context.params["ks_prev_#{key}".to_sym]
+ end
+ end
- def total_pages(paginated_data)
- # Ensure there is in total at least 1 page
- [paginated_data.total_pages, 1].max
+ # All keys relevant to pagination.
+ # This always includes the primary key. Optionally, the `order_by` key is prepended.
+ def keys
+ @keys ||= [column_for_order_by(relation), primary_key].compact.uniq
+ end
end
- def add_default_order(relation)
- if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
- relation = relation.order(:id)
+ class KeysetPaginationStrategy
+ attr_reader :request_context
+ delegate :params, :header, :request, to: :request_context
+
+ def initialize(request_context)
+ @request_context = request_context
+ end
+
+ def paginate(relation)
+ pagination = KeysetPaginationInfo.new(relation, request_context)
+
+ paged_relation = relation.limit(per_page)
+
+ if conds = conditions(pagination)
+ paged_relation = paged_relation.where(*conds)
+ end
+
+ # In all cases: sort by primary key (possibly in addition to another sort column)
+ paged_relation = paged_relation.order(pagination.primary_key => pagination.sort)
+
+ add_default_pagination_headers
+
+ if last_record = paged_relation.last
+ next_page_params = pagination.params_for(last_record)
+ add_navigation_links(next_page_params)
+ end
+
+ paged_relation
+ end
+
+ private
+
+ def conditions(pagination)
+ fields = pagination.fields
+
+ return nil if fields.empty?
+
+ placeholder = fields.map { '?' }
+
+ comp = if pagination.sort_ascending?
+ '>'
+ else
+ '<'
+ end
+
+ [
+ # Row value comparison:
+ # (A, B) < (a, b) <=> (A < a) OR (A = a AND B < b)
+ # <=> A <= a AND ((A < a) OR (A = a AND B < b))
+ "(#{fields.keys.join(',')}) #{comp} (#{placeholder.join(',')})",
+ *fields.values
+ ]
+ end
+
+ def per_page
+ params[:per_page]
+ end
+
+ def add_default_pagination_headers
+ header 'X-Per-Page', per_page.to_s
+ end
+
+ def add_navigation_links(next_page_params)
+ header 'X-Next-Page', page_href(next_page_params)
+ header 'Link', link_for('next', next_page_params)
end
- relation
+ def page_href(next_page_params)
+ request_url = request.url.split('?').first
+ request_params = params.dup
+ request_params[:per_page] = per_page
+
+ request_params.merge!(next_page_params) if next_page_params
+
+ "#{request_url}?#{request_params.to_query}"
+ end
+
+ def link_for(rel, next_page_params)
+ %(<#{page_href(next_page_params)}>; rel="#{rel}")
+ end
end
- def data_without_counts?(paginated_data)
- paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
+ class DefaultPaginationStrategy
+ attr_reader :request_context
+ delegate :params, :header, :request, to: :request_context
+
+ def initialize(request_context)
+ @request_context = request_context
+ end
+
+ def paginate(relation)
+ relation = add_default_order(relation)
+
+ relation.page(params[:page]).per(params[:per_page]).tap do |data|
+ add_pagination_headers(data)
+ end
+ end
+
+ private
+
+ def add_default_order(relation)
+ if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
+ relation = relation.order(:id)
+ end
+
+ relation
+ end
+
+ def add_pagination_headers(paginated_data)
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+
+ return if data_without_counts?(paginated_data)
+
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', total_pages(paginated_data).to_s
+ end
+
+ def pagination_links(paginated_data)
+ request_url = request.url.split('?').first
+ request_params = params.clone
+ request_params[:per_page] = paginated_data.limit_value
+
+ links = []
+
+ request_params[:page] = paginated_data.prev_page
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page]
+
+ request_params[:page] = paginated_data.next_page
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page]
+
+ request_params[:page] = 1
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
+
+ unless data_without_counts?(paginated_data)
+ request_params[:page] = total_pages(paginated_data)
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
+ end
+
+ links.join(', ')
+ end
+
+ def total_pages(paginated_data)
+ # Ensure there is in total at least 1 page
+ [paginated_data.total_pages, 1].max
+ end
+
+ def data_without_counts?(paginated_data)
+ paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
+ end
end
end
end
diff --git a/lib/api/helpers/project_snapshots_helpers.rb b/lib/api/helpers/project_snapshots_helpers.rb
new file mode 100644
index 00000000000..94798a8cb51
--- /dev/null
+++ b/lib/api/helpers/project_snapshots_helpers.rb
@@ -0,0 +1,25 @@
+module API
+ module Helpers
+ module ProjectSnapshotsHelpers
+ def authorize_read_git_snapshot!
+ authenticated_with_full_private_access!
+ end
+
+ def send_git_snapshot(repository)
+ header(*Gitlab::Workhorse.send_git_snapshot(repository))
+ end
+
+ def snapshot_project
+ user_project
+ end
+
+ def snapshot_repository
+ if to_boolean(params[:wiki])
+ snapshot_project.wiki.repository
+ else
+ snapshot_project.repository
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb
index 7f4d6e58b34..a2cbed30229 100644
--- a/lib/api/helpers/related_resources_helpers.rb
+++ b/lib/api/helpers/related_resources_helpers.rb
@@ -13,9 +13,14 @@ module API
def expose_url(path)
url_options = Gitlab::Application.routes.default_url_options
- protocol, host, port = url_options.slice(:protocol, :host, :port).values
+ protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name)
- URI::Generic.build(scheme: protocol, host: host, port: port, path: path).to_s
+ # Using a blank component at the beginning of the join we ensure
+ # that the resulted path will start with '/'. If the resulted path
+ # does not start with '/', URI::Generic#build will fail
+ path_with_script_name = File.join('', [script_name, path].select(&:present?))
+
+ URI::Generic.build(scheme: protocol, host: host, port: port, path: path_with_script_name).to_s
end
private
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 6b72caea8fd..a9803be9f69 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -59,7 +59,11 @@ module API
status: true,
gl_repository: gl_repository,
gl_username: user&.username,
- repository_path: repository_path,
+
+ # This repository_path is a bogus value but gitlab-shell still requires
+ # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135
+ repository_path: '/',
+
gitaly: gitaly_payload(params[:action])
}
end
@@ -113,7 +117,7 @@ module API
{
api_version: API.version,
gitlab_version: Gitlab::VERSION,
- gitlab_rev: Gitlab::REVISION,
+ gitlab_rev: Gitlab.revision,
redis: redis_ping
}
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 12ff2a1398b..6d75e8817c4 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -2,7 +2,7 @@ module API
class Issues < Grape::API
include PaginationParams
- before { authenticate! }
+ before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
@@ -13,6 +13,7 @@ module API
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
+ args[:scope] = args[:scope].underscore if args[:scope]
issues = IssuesFinder.new(current_user, args).execute
.preload(:assignees, :labels, :notes, :timelogs)
@@ -36,8 +37,8 @@ module API
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 :scope, type: String, values: %w[created-by-me assigned-to-me all],
- desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ 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
end
@@ -66,10 +67,11 @@ module API
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
- optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me',
- desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
+ desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
end
get do
+ authenticate! unless params[:scope] == 'all'
issues = paginate(find_issues)
options = {
diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb
new file mode 100644
index 00000000000..b9ed68aa584
--- /dev/null
+++ b/lib/api/markdown.rb
@@ -0,0 +1,33 @@
+module API
+ class Markdown < Grape::API
+ params do
+ requires :text, type: String, desc: "The markdown text to render"
+ optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown"
+ optional :project, type: String, desc: "The full path of a project to use as the context when creating references using GitLab Flavored Markdown"
+ end
+ resource :markdown do
+ desc "Render markdown text" do
+ detail "This feature was introduced in GitLab 11.0."
+ end
+ post do
+ # Explicitly set CommonMark as markdown engine to use.
+ # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done.
+ context = { markdown_engine: :common_mark, only_path: false }
+
+ if params[:project]
+ project = Project.find_by_full_path(params[:project])
+
+ not_found!("Project") unless can?(current_user, :read_project, project)
+
+ context[:project] = project
+ else
+ context[:skip_project_check] = true
+ end
+
+ context[:pipeline] = params[:gfm] ? :full : :plain_markdown
+
+ { html: Banzai.render(params[:text], context) }
+ end
+ end
+ end
+end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index d4cc18f622b..bc4df16e3a8 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -38,6 +38,7 @@ module API
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
+ args[:scope] = args[:scope].underscore if args[:scope]
merge_requests = MergeRequestsFinder.new(current_user, args).execute
.reorder(args[:order_by] => args[:sort])
@@ -79,8 +80,8 @@ module API
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 :scope, type: String, values: %w[created-by-me assigned-to-me all],
- desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ 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'
optional :source_branch, type: String, desc: 'Return merge requests with the given source branch'
optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
@@ -95,8 +96,8 @@ module API
end
params do
use :merge_requests_params
- optional :scope, type: String, values: %w[created-by-me assigned-to-me all], default: 'created-by-me',
- desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
+ desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
end
get do
authenticate! unless params[:scope] == 'all'
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
index c570eace862..a8eb137e46a 100644
--- a/lib/api/milestone_responses.rb
+++ b/lib/api/milestone_responses.rb
@@ -24,7 +24,7 @@ module API
optional :state_event, type: String, values: %w[close activate],
desc: 'The state event of the milestone '
use :optional_params
- at_least_one_of :title, :description, :due_date, :state_event
+ at_least_one_of :title, :description, :start_date, :due_date, :state_event
end
def list_milestones_for(parent)
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 69f1df6b341..39923e6d5b5 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -31,23 +31,19 @@ module API
get ":id/#{noteables_str}/:noteable_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- if can?(current_user, noteable_read_ability_name(noteable), noteable)
- # We exclude notes that are cross-references and that cannot be viewed
- # by the current user. By doing this exclusion at this level and not
- # at the DB query level (which we cannot in that case), the current
- # page can have less elements than :per_page even if
- # there's more than one page.
- raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
- notes =
- # paginate() only works with a relation. This could lead to a
- # mismatch between the pagination headers info and the actual notes
- # array returned, but this is really a edge-case.
- paginate(raw_notes)
- .reject { |n| n.cross_reference_not_visible_for?(current_user) }
- present notes, with: Entities::Note
- else
- not_found!("Notes")
- end
+ # We exclude notes that are cross-references and that cannot be viewed
+ # by the current user. By doing this exclusion at this level and not
+ # at the DB query level (which we cannot in that case), the current
+ # page can have less elements than :per_page even if
+ # there's more than one page.
+ raw_notes = noteable.notes.with_metadata.reorder(params[:order_by] => params[:sort])
+ notes =
+ # paginate() only works with a relation. This could lead to a
+ # mismatch between the pagination headers info and the actual notes
+ # array returned, but this is really a edge-case.
+ paginate(raw_notes)
+ .reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ present notes, with: Entities::Note
end
desc "Get a single #{noteable_type.to_s.downcase} note" do
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index d2b8b832e4e..735591fedd5 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -19,6 +19,7 @@ module API
optional :status, type: String, values: HasStatus::AVAILABLE_STATUSES,
desc: 'The status of pipelines'
optional :ref, type: String, desc: 'The ref of pipelines'
+ optional :sha, type: String, desc: 'The sha of pipelines'
optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations'
optional :name, type: String, desc: 'The name of the user who triggered pipelines'
optional :username, type: String, desc: 'The username of the user who triggered pipelines'
diff --git a/lib/api/project_snapshots.rb b/lib/api/project_snapshots.rb
new file mode 100644
index 00000000000..71005acc587
--- /dev/null
+++ b/lib/api/project_snapshots.rb
@@ -0,0 +1,19 @@
+module API
+ class ProjectSnapshots < Grape::API
+ helpers ::API::Helpers::ProjectSnapshotsHelpers
+
+ before { authorize_read_git_snapshot! }
+
+ resource :projects do
+ desc 'Download a (possibly inconsistent) snapshot of a repository' do
+ detail 'This feature was introduced in GitLab 10.7'
+ end
+ params do
+ optional :wiki, type: Boolean, desc: 'Set to true to receive the wiki repository'
+ end
+ get ':id/snapshot' do
+ send_git_snapshot(snapshot_repository)
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 51b3b0459f3..8871792060b 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -74,6 +74,11 @@ module API
present options[:with].prepare_relation(projects, options), options
end
+
+ def translate_params_for_compatibility(params)
+ params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled)
+ params
+ end
end
resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
@@ -123,7 +128,7 @@ module API
end
post do
attrs = declared_params(include_missing: false)
- attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
+ attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
@@ -155,6 +160,7 @@ module API
not_found!('User') unless user
attrs = declared_params(include_missing: false)
+ attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
@@ -276,7 +282,7 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility].present?
- attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled)
+ attrs = translate_params_for_compatibility(attrs)
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 4d4fbe50f9f..a7f1cb1131f 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -11,22 +11,26 @@ module API
requires :token, type: String, desc: 'Registration token'
optional :description, type: String, desc: %q(Runner's description)
optional :info, type: Hash, desc: %q(Runner's metadata)
+ optional :active, type: Boolean, desc: 'Should Runner be active'
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
post '/' do
- attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list, :maximum_timeout])
+ attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :maximum_timeout])
.merge(get_runner_details_from_request)
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
- Ci::Runner.create(attributes.merge(is_shared: true))
+ Ci::Runner.create(attributes.merge(is_shared: true, runner_type: :instance_type))
elsif project = Project.find_by(runners_token: params[:token])
- # Create a specific runner for project.
- project.runners.create(attributes)
+ # Create a specific runner for the project
+ project.runners.create(attributes.merge(runner_type: :project_type))
+ elsif group = Group.find_by(runners_token: params[:token])
+ # Create a specific runner for the group
+ group.runners.create(attributes.merge(runner_type: :group_type))
end
break forbidden! unless runner
@@ -145,14 +149,26 @@ module API
end
patch '/:id/trace' do
job = authenticate_job!
+ forbidden!('Job is not running') unless job.running?
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
content_range = request.headers['Content-Range']
content_range = content_range.split('-')
- stream_size = job.trace.append(request.body.read, content_range[0].to_i)
- if stream_size < 0
- break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
+ # TODO:
+ # it seems that `Content-Range` as formatted by runner is wrong,
+ # the `byte_end` should point to final byte, but it points byte+1
+ # that means that we have to calculate end of body,
+ # as we cannot use `content_length[1]`
+ # Issue: https://gitlab.com/gitlab-org/gitlab-runner/issues/3275
+
+ body_data = request.body.read
+ body_start = content_range[0].to_i
+ body_end = body_start + body_data.bytesize
+
+ stream_size = job.trace.append(body_data, body_start)
+ unless stream_size == body_end
+ break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{stream_size}" })
end
status 202
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 5f2a9567605..5cb96d467c0 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -14,7 +14,7 @@ module API
use :pagination
end
get do
- runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: %w(specific shared))
+ runners = filter_runners(current_user.ci_owned_runners, params[:scope], without: %w(specific shared))
present paginate(runners), with: Entities::Runner
end
@@ -184,40 +184,35 @@ module API
def authenticate_show_runner!(runner)
return if runner.is_shared || current_user.admin?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end
def authenticate_update_runner!(runner)
return if current_user.admin?
- forbidden!("Runner is shared") if runner.is_shared?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("No access granted") unless can?(current_user, :update_runner, runner)
end
def authenticate_delete_runner!(runner)
return if current_user.admin?
- forbidden!("Runner is shared") if runner.is_shared?
forbidden!("Runner associated with more than one project") if runner.projects.count > 1
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("No access granted") unless can?(current_user, :delete_runner, runner)
end
def authenticate_enable_runner!(runner)
- forbidden!("Runner is shared") if runner.is_shared?
- forbidden!("Runner is locked") if runner.locked?
+ forbidden!("Runner is a group runner") if runner.group_type?
+
return if current_user.admin?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
+ forbidden!("Runner is locked") if runner.locked?
+ forbidden!("No access granted") unless can?(current_user, :assign_runner, runner)
end
def authenticate_list_runners_jobs!(runner)
return if current_user.admin?
- forbidden!("No access granted") unless user_can_access_runner?(runner)
- end
-
- def user_can_access_runner?(runner)
- current_user.ci_authorized_runners.exists?(runner.id)
+ forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
end
end
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 152df23a327..e31c332b6e4 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -5,7 +5,7 @@ module API
helpers do
def current_settings
@current_setting ||=
- (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults)
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3920171205f..14b8a796c8e 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -77,7 +77,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin?
- params.except!(:created_after, :created_before, :order_by, :sort)
+ params.except!(:created_after, :created_before, :order_by, :sort, :two_factor)
end
users = UsersFinder.new(current_user, params).execute
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 2c52d21fa1c..4fa7d196e50 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -131,7 +131,9 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
- present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
+ ::Groups::DestroyService.new(group, current_user).async_execute
+
+ accepted!
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
index c6d9957d452..8a5c46805bd 100644
--- a/lib/api/v3/runners.rb
+++ b/lib/api/v3/runners.rb
@@ -58,7 +58,7 @@ module API
end
def user_can_access_runner?(runner)
- current_user.ci_authorized_runners.exists?(runner.id)
+ current_user.ci_owned_runners.exists?(runner.id)
end
end
end
diff --git a/lib/api/v3/settings.rb b/lib/api/v3/settings.rb
index 9b4ab7630fb..fc56495c8b1 100644
--- a/lib/api/v3/settings.rb
+++ b/lib/api/v3/settings.rb
@@ -6,7 +6,7 @@ module API
helpers do
def current_settings
@current_setting ||=
- (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ (ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults)
end
end
diff --git a/lib/api/version.rb b/lib/api/version.rb
index 9ba576bd828..3b10bfa6a7d 100644
--- a/lib/api/version.rb
+++ b/lib/api/version.rb
@@ -6,7 +6,7 @@ module API
detail 'This feature was introduced in GitLab 8.13.'
end
get '/version' do
- { version: Gitlab::VERSION, revision: Gitlab::REVISION }
+ { version: Gitlab::VERSION, revision: Gitlab.revision }
end
end
end
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 6a5a223a614..45a935ab352 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Artifacts < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('artifacts', JobArtifactUploader.root)
end
end
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
index f869916e199..adf85ca4719 100644
--- a/lib/backup/builds.rb
+++ b/lib/backup/builds.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Builds < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('builds', Settings.gitlab_ci.builds_path)
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 5e6828de597..1608f7ad02d 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -2,9 +2,11 @@ require 'yaml'
module Backup
class Database
+ attr_reader :progress
attr_reader :config, :db_file_name
- def initialize
+ def initialize(progress)
+ @progress = progress
@config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
@db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
@@ -19,12 +21,12 @@ module Backup
dump_pid =
case config["adapter"]
when /^mysql/ then
- $progress.print "Dumping MySQL database #{config['database']} ... "
+ progress.print "Dumping MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
when "postgresql" then
- $progress.print "Dumping PostgreSQL database #{config['database']} ... "
+ progress.print "Dumping PostgreSQL database #{config['database']} ... "
pg_env
pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
if Gitlab.config.backup.pg_schema
@@ -53,12 +55,12 @@ module Backup
restore_pid =
case config["adapter"]
when /^mysql/ then
- $progress.print "Restoring MySQL database #{config['database']} ... "
+ progress.print "Restoring MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
when "postgresql" then
- $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+ progress.print "Restoring PostgreSQL database #{config['database']} ... "
pg_env
spawn('psql', config['database'], in: decompress_rd)
end
@@ -111,9 +113,9 @@ module Backup
def report_success(success)
if success
- $progress.puts '[DONE]'.color(:green)
+ progress.puts '[DONE]'.color(:green)
else
- $progress.puts '[FAILED]'.color(:red)
+ progress.puts '[FAILED]'.color(:red)
end
end
end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 88cb7e7b5a4..9895db9e451 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -53,6 +53,8 @@ module Backup
FileUtils.mv(files, timestamped_files_path)
rescue Errno::EACCES
access_denied_error(app_files_dir)
+ rescue Errno::EBUSY
+ resource_busy_error(app_files_dir)
end
end
end
diff --git a/lib/backup/helper.rb b/lib/backup/helper.rb
index a1ee0faefe9..54b9ce10b4d 100644
--- a/lib/backup/helper.rb
+++ b/lib/backup/helper.rb
@@ -13,5 +13,19 @@ module Backup
EOS
raise message
end
+
+ def resource_busy_error(path)
+ message = <<~EOS
+
+ ### NOTICE ###
+ As part of restore, the task tried to rename `#{path}` before restoring.
+ This could not be completed, perhaps `#{path}` is a mountpoint?
+
+ To complete the restore, please move the contents of `#{path}` to a
+ different location and run the restore task again.
+
+ EOS
+ raise message
+ end
end
end
diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb
index 4e234e50a7a..185ff8ae6bd 100644
--- a/lib/backup/lfs.rb
+++ b/lib/backup/lfs.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Lfs < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('lfs', Settings.lfs.storage_path)
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index f27ce4d2b2b..a8da0c7edef 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -4,6 +4,12 @@ module Backup
FOLDERS_TO_BACKUP = %w[repositories db].freeze
FILE_NAME_SUFFIX = '_gitlab_backup.tar'.freeze
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+ end
+
def pack
# Make sure there is a connection
ActiveRecord::Base.connection.reconnect!
@@ -14,11 +20,11 @@ module Backup
end
# create archive
- $progress.print "Creating backup archive: #{tar_file} ... "
+ progress.print "Creating backup archive: #{tar_file} ... "
# Set file permissions on open to prevent chmod races.
tar_system_options = { out: [tar_file, 'w', Gitlab.config.backup.archive_permissions] }
if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options)
- $progress.puts "done".color(:green)
+ progress.puts "done".color(:green)
else
puts "creating archive #{tar_file} failed".color(:red)
abort 'Backup failed'
@@ -29,11 +35,11 @@ module Backup
end
def upload
- $progress.print "Uploading backup archive to remote storage #{remote_directory} ... "
+ progress.print "Uploading backup archive to remote storage #{remote_directory} ... "
connection_settings = Gitlab.config.backup.upload.connection
if connection_settings.blank?
- $progress.puts "skipped".color(:yellow)
+ progress.puts "skipped".color(:yellow)
return
end
@@ -43,7 +49,7 @@ module Backup
multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
encryption: Gitlab.config.backup.upload.encryption,
storage_class: Gitlab.config.backup.upload.storage_class)
- $progress.puts "done".color(:green)
+ progress.puts "done".color(:green)
else
puts "uploading backup to #{remote_directory} failed".color(:red)
abort 'Backup failed'
@@ -51,13 +57,13 @@ module Backup
end
def cleanup
- $progress.print "Deleting tmp directories ... "
+ progress.print "Deleting tmp directories ... "
backup_contents.each do |dir|
next unless File.exist?(File.join(backup_path, dir))
if FileUtils.rm_rf(File.join(backup_path, dir))
- $progress.puts "done".color(:green)
+ progress.puts "done".color(:green)
else
puts "deleting tmp directory '#{dir}' failed".color(:red)
abort 'Backup failed'
@@ -67,7 +73,7 @@ module Backup
def remove_old
# delete backups
- $progress.print "Deleting old backups ... "
+ progress.print "Deleting old backups ... "
keep_time = Gitlab.config.backup.keep_time.to_i
if keep_time > 0
@@ -88,31 +94,32 @@ module Backup
FileUtils.rm(file)
removed += 1
rescue => e
- $progress.puts "Deleting #{file} failed: #{e.message}".color(:red)
+ progress.puts "Deleting #{file} failed: #{e.message}".color(:red)
end
end
end
end
- $progress.puts "done. (#{removed} removed)".color(:green)
+ progress.puts "done. (#{removed} removed)".color(:green)
else
- $progress.puts "skipping".color(:yellow)
+ progress.puts "skipping".color(:yellow)
end
end
+ # rubocop: disable Metrics/AbcSize
def unpack
Dir.chdir(backup_path) do
# check for existing backups in the backup dir
if backup_file_list.empty?
- $progress.puts "No backups found in #{backup_path}"
- $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
+ progress.puts "No backups found in #{backup_path}"
+ progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}"
exit 1
elsif backup_file_list.many? && ENV["BACKUP"].nil?
- $progress.puts 'Found more than one backup:'
+ progress.puts 'Found more than one backup:'
# print list of available backups
- $progress.puts " " + available_timestamps.join("\n ")
- $progress.puts 'Please specify which one you want to restore:'
- $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
+ progress.puts " " + available_timestamps.join("\n ")
+ progress.puts 'Please specify which one you want to restore:'
+ progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup'
exit 1
end
@@ -123,31 +130,31 @@ module Backup
end
unless File.exist?(tar_file)
- $progress.puts "The backup file #{tar_file} does not exist!"
+ progress.puts "The backup file #{tar_file} does not exist!"
exit 1
end
- $progress.print 'Unpacking backup ... '
+ progress.print 'Unpacking backup ... '
unless Kernel.system(*%W(tar -xf #{tar_file}))
- $progress.puts 'unpacking backup failed'.color(:red)
+ progress.puts 'unpacking backup failed'.color(:red)
exit 1
else
- $progress.puts 'done'.color(:green)
+ progress.puts 'done'.color(:green)
end
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems
if settings[:gitlab_version] != Gitlab::VERSION
- $progress.puts(<<~HEREDOC.color(:red))
+ progress.puts(<<~HEREDOC.color(:red))
GitLab version mismatch:
Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!
Please switch to the following version and try again:
version: #{settings[:gitlab_version]}
HEREDOC
- $progress.puts
- $progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
+ progress.puts
+ progress.puts "Hint: git checkout v#{settings[:gitlab_version]}"
exit 1
end
end
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
index 5830b209d6e..542e35a7c7c 100644
--- a/lib/backup/pages.rb
+++ b/lib/backup/pages.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Pages < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('pages', Gitlab.config.pages.path)
end
end
diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb
index 91698669402..35821805797 100644
--- a/lib/backup/registry.rb
+++ b/lib/backup/registry.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Registry < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('registry', Settings.registry.path)
end
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 89e3f1d9076..84670d6582e 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -6,6 +6,12 @@ module Backup
include Backup::Helper
# rubocop:disable Metrics/AbcSize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+ end
+
def dump
prepare
@@ -68,87 +74,97 @@ module Backup
def prepare_directories
Gitlab.config.repositories.storages.each do |name, repository_storage|
- path = repository_storage.legacy_disk_path
- next unless File.exist?(path)
-
- # Move all files in the existing repos directory except . and .. to
- # repositories.old.<timestamp> directory
- bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
- FileUtils.mkdir_p(bk_repos_path, mode: 0700)
- files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
-
- begin
- FileUtils.mv(files, bk_repos_path)
- rescue Errno::EACCES
- access_denied_error(path)
+ delete_all_repositories(name, repository_storage)
+ end
+ end
+
+ def delete_all_repositories(name, repository_storage)
+ gitaly_migrate(:delete_all_repositories) do |is_enabled|
+ if is_enabled
+ Gitlab::GitalyClient::StorageService.new(name).delete_all_repositories
+ else
+ local_delete_all_repositories(name, repository_storage)
+ end
+ end
+ end
+
+ def local_delete_all_repositories(name, repository_storage)
+ path = repository_storage.legacy_disk_path
+ return unless File.exist?(path)
+
+ # Move all files in the existing repos directory except . and .. to
+ # repositories.old.<timestamp> directory
+ bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
+ FileUtils.mkdir_p(bk_repos_path, mode: 0700)
+ files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
+
+ begin
+ FileUtils.mv(files, bk_repos_path)
+ rescue Errno::EACCES
+ access_denied_error(path)
+ rescue Errno::EBUSY
+ resource_busy_error(path)
+ end
+ end
+
+ def restore_custom_hooks(project)
+ # TODO: Need to find a way to do this for gitaly
+ # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/1195
+ in_path(path_to_tars(project)) do |dir|
+ path_to_project_repo = path_to_repo(project)
+ cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
+
+ output, status = Gitlab::Popen.popen(cmd)
+ unless status.zero?
+ progress_warn(project, cmd.join(' '), output)
end
end
end
def restore
prepare_directories
+ gitlab_shell = Gitlab::Shell.new
+
Project.find_each(batch_size: 1000) do |project|
- progress.print " * #{display_repo_path(project)} ... "
- path_to_project_repo = path_to_repo(project)
+ progress.print " * #{project.full_path} ... "
path_to_project_bundle = path_to_bundle(project)
-
project.ensure_storage_path_exists
- cmd = if File.exist?(path_to_project_bundle)
- %W(#{Gitlab.config.git.bin_path} clone --bare --mirror #{path_to_project_bundle} #{path_to_project_repo})
- else
- %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
- end
+ restore_repo_success = nil
+ if File.exist?(path_to_project_bundle)
+ begin
+ project.repository.create_from_bundle path_to_project_bundle
+ restore_repo_success = true
+ rescue => e
+ restore_repo_success = false
+ progress.puts "Error: #{e}".color(:red)
+ end
+ else
+ restore_repo_success = gitlab_shell.create_repository(project.repository_storage, project.disk_path)
+ end
- output, status = Gitlab::Popen.popen(cmd)
- if status.zero?
+ if restore_repo_success
progress.puts "[DONE]".color(:green)
else
- progress_warn(project, cmd.join(' '), output)
+ progress.puts "[Failed] restoring #{project.full_path} repository".color(:red)
end
- in_path(path_to_tars(project)) do |dir|
- cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
-
- output, status = Gitlab::Popen.popen(cmd)
- unless status.zero?
- progress_warn(project, cmd.join(' '), output)
- end
- end
+ restore_custom_hooks(project)
wiki = ProjectWiki.new(project)
- path_to_wiki_repo = path_to_repo(wiki)
path_to_wiki_bundle = path_to_bundle(wiki)
if File.exist?(path_to_wiki_bundle)
- progress.print " * #{display_repo_path(wiki)} ... "
-
- # If a wiki bundle exists, first remove the empty repo
- # that was initialized with ProjectWiki.new() and then
- # try to restore with 'git clone --bare'.
- FileUtils.rm_rf(path_to_wiki_repo)
- cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_wiki_bundle} #{path_to_wiki_repo})
-
- output, status = Gitlab::Popen.popen(cmd)
- if status.zero?
- progress.puts " [DONE]".color(:green)
- else
- progress_warn(project, cmd.join(' '), output)
+ progress.print " * #{wiki.full_path} ... "
+ begin
+ wiki.repository.create_from_bundle(path_to_wiki_bundle)
+ progress.puts "[DONE]".color(:green)
+ rescue => e
+ progress.puts "[Failed] restoring #{wiki.full_path} wiki".color(:red)
+ progress.puts "Error #{e}".color(:red)
end
end
end
-
- progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow)
- cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args
-
- output, status = Gitlab::Popen.popen(cmd)
- if status.zero?
- progress.puts " [DONE]".color(:green)
- else
- puts " [FAILED]".color(:red)
- puts "failed: #{cmd}"
- puts output
- end
end
# rubocop:enable Metrics/AbcSize
@@ -215,12 +231,14 @@ module Backup
Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path }
end
- def progress
- $progress
- end
-
def display_repo_path(project)
project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path
end
+
+ def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
+ Gitlab::GitalyClient.migrate(method, status: status, &block)
+ rescue GRPC::NotFound, GRPC::BadStatus => e
+ raise Error, e
+ end
end
end
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index d46e2cd869d..49b117a7ee3 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -2,7 +2,11 @@ require 'backup/files'
module Backup
class Uploads < Files
- def initialize
+ attr_reader :progress
+
+ def initialize(progress)
+ @progress = progress
+
super('uploads', Rails.root.join('public/uploads'))
end
end
diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb
index ef16df1f3ae..7b55e8b36f6 100644
--- a/lib/banzai/filter/commit_trailers_filter.rb
+++ b/lib/banzai/filter/commit_trailers_filter.rb
@@ -13,7 +13,6 @@ module Banzai
# * https://git.wiki.kernel.org/index.php/CommitMessageConventions
class CommitTrailersFilter < HTML::Pipeline::Filter
include ActionView::Helpers::TagHelper
- include ApplicationHelper
include AvatarsHelper
TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index f2e9a5a1116..4bc82ecb4d6 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -58,6 +58,9 @@ module Banzai
def call
doc.search(".//text()").each do |node|
+ # Do not perform linking inside <code> blocks
+ next unless node.ancestors('code').empty?
+
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
index 5325819d828..28933c78966 100644
--- a/lib/banzai/filter/plantuml_filter.rb
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -23,7 +23,7 @@ module Banzai
private
def settings
- ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ Gitlab::CurrentSettings.current_application_settings
end
def plantuml_setup
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index b9d5ecf70ec..2f023f4f242 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -73,7 +73,7 @@ module Banzai
#
# Note that while the key might exist, its value could be nil!
def validate
- needs :project
+ needs :project unless skip_project_check?
end
# Iterates over all <a> and text() nodes in a document.
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 8b2f05fffec..a1f24e8b093 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -42,9 +42,9 @@ module Banzai
end
def self.transform_context(context)
- context.merge(
- only_path: true,
+ context[:only_path] = true unless context.key?(:only_path)
+ context.merge(
# EmojiFilter
asset_host: Gitlab::Application.config.asset_host,
asset_root: Gitlab.config.gitlab.base_url
diff --git a/lib/feature.rb b/lib/feature.rb
index 8e9ba5c530a..6474de6e56d 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -1,3 +1,6 @@
+require 'flipper/adapters/active_record'
+require 'flipper/adapters/active_support_cache_store'
+
class Feature
# Classes to override flipper table names
class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature
@@ -60,7 +63,8 @@ class Feature
end
def flipper
- @flipper ||= Flipper.instance
+ Thread.current[:flipper] ||=
+ Flipper.new(flipper_adapter).tap { |flip| flip.memoize = true }
end
# This method is called from config/initializers/flipper.rb and can be used
@@ -68,5 +72,16 @@ class Feature
# See https://docs.gitlab.com/ee/development/feature_flags.html#feature-groups
def register_feature_groups
end
+
+ def flipper_adapter
+ active_record_adapter = Flipper::Adapters::ActiveRecord.new(
+ feature_class: FlipperFeature,
+ gate_class: FlipperGate)
+
+ Flipper::Adapters::ActiveSupportCacheStore.new(
+ active_record_adapter,
+ Rails.cache,
+ expires_in: 1.hour)
+ end
end
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index f6629982512..a129746e2c6 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -1,9 +1,38 @@
-require_dependency 'gitlab/git'
+require_dependency 'gitlab/popen'
module Gitlab
+ def self.root
+ Pathname.new(File.expand_path('..', __dir__))
+ end
+
+ def self.config
+ Settings
+ end
+
+ def self.migrations_hash
+ @_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s)
+ end
+
+ def self.revision
+ @_revision ||= begin
+ if File.exist?(root.join("REVISION"))
+ File.read(root.join("REVISION")).strip.freeze
+ else
+ result = Gitlab::Popen.popen_with_detail(%W[#{config.git.bin_path} log --pretty=format:%h -n 1])
+
+ if result.status.success?
+ result.stdout.chomp.freeze
+ else
+ "Unknown".freeze
+ end
+ end
+ end
+ end
+
COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
+ VERSION = File.read(root.join("VERSION")).strip.freeze
def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com
@@ -19,6 +48,6 @@ module Gitlab
end
def self.dev_env_or_com?
- Rails.env.test? || Rails.env.development? || org? || com?
+ Rails.env.development? || org? || com?
end
end
diff --git a/lib/gitlab/auth/blocked_user_tracker.rb b/lib/gitlab/auth/blocked_user_tracker.rb
index dae03a179e4..7609a7b04f6 100644
--- a/lib/gitlab/auth/blocked_user_tracker.rb
+++ b/lib/gitlab/auth/blocked_user_tracker.rb
@@ -17,7 +17,9 @@ module Gitlab
# message passed along by Warden.
return unless message == User::BLOCKED_MESSAGE
- login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
+ # Check for either LDAP or regular GitLab account logins
+ login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'username') ||
+ env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
return unless login.present?
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
index 34286900e72..865185eb5db 100644
--- a/lib/gitlab/auth/ldap/access.rb
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -6,7 +6,7 @@ module Gitlab
module Auth
module LDAP
class Access
- attr_reader :provider, :user
+ attr_reader :provider, :user, :ldap_identity
def self.open(user, &block)
Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
@@ -14,9 +14,12 @@ module Gitlab
end
end
- def self.allowed?(user)
+ def self.allowed?(user, options = {})
self.open(user) do |access|
+ # Whether user is allowed, or not, we should update
+ # permissions to keep things clean
if access.allowed?
+ access.update_user
Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
true
@@ -29,7 +32,8 @@ module Gitlab
def initialize(user, adapter = nil)
@adapter = adapter
@user = user
- @provider = user.ldap_identity.provider
+ @ldap_identity = user.ldap_identity
+ @provider = adapter&.provider || ldap_identity&.provider
end
def allowed?
@@ -40,7 +44,7 @@ module Gitlab
end
# Block user in GitLab if he/she was blocked in AD
- if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
+ if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter)
block_user(user, 'is disabled in Active Directory')
false
else
@@ -64,27 +68,44 @@ module Gitlab
Gitlab::Auth::LDAP::Config.new(provider)
end
+ def find_ldap_user
+ Gitlab::Auth::LDAP::Person.find_by_dn(ldap_identity.extern_uid, adapter)
+ end
+
def ldap_user
- @ldap_user ||= Gitlab::Auth::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ return unless provider
+
+ @ldap_user ||= find_ldap_user
end
def block_user(user, reason)
user.ldap_block
- Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
- "blocking Gitlab user \"#{user.name}\" (#{user.email})"
- )
+ if provider
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ else
+ Gitlab::AppLogger.info(
+ "Account is not provided by LDAP, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
end
def unblock_user(user, reason)
user.activate
Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
+
+ def update_user
+ # no-op in CE
+ end
end
end
end
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index 77185f52ced..d4415eaa6dc 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -11,6 +11,8 @@ module Gitlab
attr_accessor :provider, :options
+ InvalidProvider = Class.new(StandardError)
+
def self.enabled?
Gitlab.config.ldap.enabled
end
@@ -22,6 +24,10 @@ module Gitlab
def self.available_servers
return [] unless enabled?
+ _available_servers
+ end
+
+ def self._available_servers
Array.wrap(servers.first)
end
@@ -34,7 +40,7 @@ module Gitlab
end
def self.invalid_provider(provider)
- raise "Unknown provider (#{provider}). Available providers: #{providers}"
+ raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}")
end
def initialize(provider)
@@ -84,13 +90,17 @@ module Gitlab
end
def base
- options['base']
+ @base ||= Person.normalize_dn(options['base'])
end
def uid
options['uid']
end
+ def label
+ options['label']
+ end
+
def sync_ssh_keys?
sync_ssh_keys.present?
end
@@ -132,6 +142,10 @@ module Gitlab
options['timeout'].to_i
end
+ def external_groups
+ options['external_groups'] || []
+ end
+
def has_auth?
options['password'] || options['bind_dn']
end
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
index 068212d9a21..922d0567d99 100644
--- a/lib/gitlab/auth/ldap/user.rb
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -8,6 +8,8 @@ module Gitlab
module Auth
module LDAP
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
class << self
def find_by_uid_and_provider(uid, provider)
identity = ::Identity.with_extern_uid(provider, uid).take
@@ -29,7 +31,8 @@ module Gitlab
self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
- def changed?
+ override :should_save?
+ def should_save?
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
@@ -41,6 +44,10 @@ module Gitlab
Gitlab::Auth::LDAP::Access.allowed?(gl_user)
end
+ def valid_sign_in?
+ allowed? && super
+ end
+
def ldap_config
Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
end
diff --git a/lib/gitlab/auth/o_auth/identity_linker.rb b/lib/gitlab/auth/o_auth/identity_linker.rb
new file mode 100644
index 00000000000..de92d7a214d
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module OAuth
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index d0c6b0386ba..6c5d0788a0a 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -30,6 +30,10 @@ module Gitlab
gl_user.try(:valid?)
end
+ def valid_sign_in?
+ valid? && persisted?
+ end
+
def save(provider = 'OAuth')
raise SigninDisabledForProviderError if oauth_provider_disabled?
raise SignupDisabledError unless gl_user
@@ -64,8 +68,18 @@ module Gitlab
user
end
+ def find_and_update!
+ save if should_save?
+
+ gl_user
+ end
+
protected
+ def should_save?
+ true
+ end
+
def add_or_update_user_identities
return unless gl_user
diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb
new file mode 100644
index 00000000000..f79ce6bb809
--- /dev/null
+++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb
@@ -0,0 +1,51 @@
+module Gitlab
+ module Auth
+ class OmniauthIdentityLinkerBase
+ attr_reader :current_user, :oauth
+
+ def initialize(current_user, oauth)
+ @current_user = current_user
+ @oauth = oauth
+ @changed = false
+ end
+
+ def link
+ save if identity.new_record?
+ end
+
+ def changed?
+ @changed
+ end
+
+ def failed?
+ error_message.present?
+ end
+
+ def error_message
+ identity.validate
+
+ identity.errors.full_messages.join(', ')
+ end
+
+ private
+
+ def save
+ @changed = identity.save
+ end
+
+ def identity
+ @identity ||= current_user.identities
+ .with_extern_uid(provider, uid)
+ .first_or_initialize(extern_uid: uid)
+ end
+
+ def provider
+ oauth['provider']
+ end
+
+ def uid
+ oauth['uid']
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 2760b1a3247..5fa9581f837 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -14,6 +14,10 @@ module Gitlab
def external_groups
options[:external_groups]
end
+
+ def admin_groups
+ options[:admin_groups]
+ end
end
end
end
diff --git a/lib/gitlab/auth/saml/identity_linker.rb b/lib/gitlab/auth/saml/identity_linker.rb
new file mode 100644
index 00000000000..7e4b191d512
--- /dev/null
+++ b/lib/gitlab/auth/saml/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module Saml
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index d4024e9ec39..b8c84c37cd5 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -7,6 +7,8 @@ module Gitlab
module Auth
module Saml
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
def save
super('SAML')
end
@@ -18,16 +20,15 @@ module Gitlab
user ||= find_or_build_ldap_user if auto_link_ldap_user?
user ||= build_new_user if signup_enabled?
- if external_users_enabled? && user
- # Check if there is overlap between the user's groups and the external groups
- # setting then set user as external or internal.
- user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty?
+ if user
+ user.external = !(auth_hash.groups & saml_config.external_groups).empty? if external_users_enabled?
end
user
end
- def changed?
+ override :should_save?
+ def should_save?
return true unless gl_user
gl_user.changed? || gl_user.identities.any?(&:changed?)
@@ -35,12 +36,16 @@ module Gitlab
protected
+ def saml_config
+ Gitlab::Auth::Saml::Config
+ end
+
def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user
end
def external_users_enabled?
- !Gitlab::Auth::Saml::Config.external_groups.nil?
+ !saml_config.external_groups.nil?
end
def auth_hash=(auth_hash)
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
new file mode 100644
index 00000000000..1893cb001b2
--- /dev/null
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Auth
+ class UserAccessDeniedReason
+ def initialize(user)
+ @user = user
+ end
+
+ def rejection_message
+ case rejection_type
+ when :internal
+ "This action cannot be performed by internal users"
+ when :terms_not_accepted
+ "You (#{@user.to_reference}) must accept the Terms of Service in order to perform this action. "\
+ "Please access GitLab from a web browser to accept these terms."
+ else
+ "Your account has been blocked."
+ end
+ end
+
+ private
+
+ def rejection_type
+ if @user.internal?
+ :internal
+ elsif @user.required_terms_not_accepted?
+ :terms_not_accepted
+ else
+ :blocked
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index cf02030c577..4dc23f977da 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -1,9 +1,5 @@
module Gitlab
module Auth
- #
- # Exceptions
- #
-
AuthenticationError = Class.new(StandardError)
MissingTokenError = Class.new(AuthenticationError)
TokenNotFoundError = Class.new(AuthenticationError)
@@ -61,6 +57,12 @@ module Gitlab
private
+ def route_authentication_setting
+ return {} unless respond_to?(:route_setting)
+
+ route_setting(:authentication) || {}
+ end
+
def access_token
strong_memoize(:access_token) do
find_oauth_access_token || find_personal_access_token
diff --git a/lib/gitlab/background_migration/migrate_stage_index.rb b/lib/gitlab/background_migration/migrate_stage_index.rb
new file mode 100644
index 00000000000..f90f35a913d
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_stage_index.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class MigrateStageIndex
+ def perform(start_id, stop_id)
+ migrate_stage_index_sql(start_id.to_i, stop_id.to_i).tap do |sql|
+ ActiveRecord::Base.connection.execute(sql)
+ end
+ end
+
+ private
+
+ def migrate_stage_index_sql(start_id, stop_id)
+ if Gitlab::Database.postgresql?
+ <<~SQL
+ WITH freqs AS (
+ SELECT stage_id, stage_idx, COUNT(*) AS freq FROM ci_builds
+ WHERE stage_id BETWEEN #{start_id} AND #{stop_id}
+ AND stage_idx IS NOT NULL
+ GROUP BY stage_id, stage_idx
+ ), indexes AS (
+ SELECT DISTINCT stage_id, last_value(stage_idx)
+ OVER (PARTITION BY stage_id ORDER BY freq ASC) AS index
+ FROM freqs
+ )
+
+ UPDATE ci_stages SET position = indexes.index
+ FROM indexes WHERE indexes.stage_id = ci_stages.id
+ AND ci_stages.position IS NULL;
+ SQL
+ else
+ <<~SQL
+ UPDATE ci_stages
+ SET position =
+ (SELECT stage_idx FROM ci_builds
+ WHERE ci_builds.stage_id = ci_stages.id
+ GROUP BY ci_builds.stage_idx ORDER BY COUNT(*) DESC LIMIT 1)
+ WHERE ci_stages.id BETWEEN #{start_id} AND #{stop_id}
+ AND ci_stages.position IS NULL
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/populate_import_state.rb b/lib/gitlab/background_migration/populate_import_state.rb
new file mode 100644
index 00000000000..695a2a713c5
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_import_state.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This background migration creates all the records on the
+ # import state table for projects that are considered imports or forks
+ class PopulateImportState
+ def perform(start_id, end_id)
+ move_attributes_data_to_import_state(start_id, end_id)
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def move_attributes_data_to_import_state(start_id, end_id)
+ Rails.logger.info("#{self.class.name} - Moving import attributes data to project mirror data table: #{start_id} - #{end_id}")
+
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO project_mirror_data (project_id, status, jid, last_error)
+ SELECT id, import_status, import_jid, import_error
+ FROM projects
+ WHERE projects.import_status != 'none'
+ AND projects.id BETWEEN #{start_id} AND #{end_id}
+ AND NOT EXISTS (
+ SELECT id
+ FROM project_mirror_data
+ WHERE project_id = projects.id
+ )
+ SQL
+
+ ActiveRecord::Base.connection.execute <<~SQL
+ UPDATE projects
+ SET import_status = 'none'
+ WHERE import_status != 'none'
+ AND id BETWEEN #{start_id} AND #{end_id}
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/rollback_import_state_data.rb b/lib/gitlab/background_migration/rollback_import_state_data.rb
new file mode 100644
index 00000000000..a7c986747d8
--- /dev/null
+++ b/lib/gitlab/background_migration/rollback_import_state_data.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This background migration migrates all the data of import_state
+ # back to the projects table for projects that are considered imports or forks
+ class RollbackImportStateData
+ def perform(start_id, end_id)
+ move_attributes_data_to_project(start_id, end_id)
+ end
+
+ def move_attributes_data_to_project(start_id, end_id)
+ Rails.logger.info("#{self.class.name} - Moving import attributes data to projects table: #{start_id} - #{end_id}")
+
+ if Gitlab::Database.mysql?
+ ActiveRecord::Base.connection.execute <<~SQL
+ UPDATE projects, project_mirror_data
+ SET
+ projects.import_status = project_mirror_data.status,
+ projects.import_jid = project_mirror_data.jid,
+ projects.import_error = project_mirror_data.last_error
+ WHERE project_mirror_data.project_id = projects.id
+ AND project_mirror_data.id BETWEEN #{start_id} AND #{end_id}
+ SQL
+ else
+ ActiveRecord::Base.connection.execute <<~SQL
+ UPDATE projects
+ SET
+ import_status = project_mirror_data.status,
+ import_jid = project_mirror_data.jid,
+ import_error = project_mirror_data.last_error
+ FROM project_mirror_data
+ WHERE project_mirror_data.project_id = projects.id
+ AND project_mirror_data.id BETWEEN #{start_id} AND #{end_id}
+ SQL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb
index 1a25138e7d6..4ca5a78e068 100644
--- a/lib/gitlab/bare_repository_import/importer.rb
+++ b/lib/gitlab/bare_repository_import/importer.rb
@@ -75,10 +75,11 @@ module Gitlab
end
def mv_repo(project)
- FileUtils.mv(repo_path, File.join(project.repository_storage_path, project.disk_path + '.git'))
+ storage_path = storage_path_for_shard(project.repository_storage)
+ FileUtils.mv(repo_path, project.repository.path_to_repo)
if bare_repo.wiki_exists?
- FileUtils.mv(wiki_path, File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ FileUtils.mv(wiki_path, File.join(storage_path, project.disk_path + '.wiki.git'))
end
true
@@ -88,6 +89,10 @@ module Gitlab
false
end
+ def storage_path_for_shard(shard)
+ Gitlab.config.repositories.storages[shard].legacy_disk_path
+ end
+
def find_or_create_groups
return nil unless group_path.present?
diff --git a/lib/gitlab/base_doorkeeper_controller.rb b/lib/gitlab/base_doorkeeper_controller.rb
new file mode 100644
index 00000000000..e4227af25d2
--- /dev/null
+++ b/lib/gitlab/base_doorkeeper_controller.rb
@@ -0,0 +1,8 @@
+# This is a base controller for doorkeeper.
+# It adds the `can?` helper used in the views.
+module Gitlab
+ class BaseDoorkeeperController < ActionController::Base
+ include Gitlab::Allowable
+ helper_method :can?
+ end
+end
diff --git a/lib/gitlab/build_access.rb b/lib/gitlab/build_access.rb
new file mode 100644
index 00000000000..08a8f846ca5
--- /dev/null
+++ b/lib/gitlab/build_access.rb
@@ -0,0 +1,12 @@
+module Gitlab
+ class BuildAccess < UserAccess
+ attr_accessor :user, :project
+
+ # This bypasses the `can?(:access_git)`-check we normally do in `UserAccess`
+ # for CI. That way if a user was able to trigger a pipeline, then the
+ # build is allowed to clone the project.
+ def can_access_git?
+ true
+ end
+ end
+end
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index 551483d0aaa..73f36735e35 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -6,7 +6,7 @@ module Gitlab
def initialize(cron, cron_timezone = 'UTC')
@cron = cron
- @cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name
+ @cron_timezone = timezone_name(cron_timezone)
end
def next_time_from(time)
@@ -24,6 +24,12 @@ module Gitlab
private
+ def timezone_name(timezone)
+ ActiveSupport::TimeZone.find_tzinfo(timezone).name
+ rescue TZInfo::InvalidTimezoneIdentifier
+ timezone
+ end
+
# NOTE:
# cron_timezone can only accept timezones listed in TZInfo::Timezone.
# Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted,
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index 70732d26bbd..b5eb0cfa2f0 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -14,7 +14,8 @@ module Gitlab
trigger_requests: Array(@command.trigger_request),
user: @command.current_user,
pipeline_schedule: @command.schedule,
- protected: @command.protected_ref?
+ protected: @command.protected_ref?,
+ variables_attributes: Array(@command.variables_attributes)
)
@pipeline.set_config_source
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index a1849b01c5d..a53c80d34f7 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -7,7 +7,7 @@ module Gitlab # rubocop:disable Naming/FileName
:origin_ref, :checkout_sha, :after_sha, :before_sha,
:trigger_request, :schedule,
:ignore_skip_ci, :save_incompleted,
- :seeds_block
+ :seeds_block, :variables_attributes
) do
include Gitlab::Utils::StrongMemoize
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index d299a5677de..69b8a8fc68f 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -14,14 +14,10 @@ module Gitlab
@command.seeds_block&.call(pipeline)
##
- # Populate pipeline with all stages and builds from pipeline seeds.
+ # Populate pipeline with all stages, and stages with builds.
#
pipeline.stage_seeds.each do |stage|
pipeline.stages << stage.to_resource
-
- stage.seeds.each do |build|
- pipeline.builds << build.to_resource
- end
end
if pipeline.stages.none?
diff --git a/lib/gitlab/ci/pipeline/expression.rb b/lib/gitlab/ci/pipeline/expression.rb
new file mode 100644
index 00000000000..f57df7c5637
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression.rb
@@ -0,0 +1,10 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ ExpressionError = Class.new(StandardError)
+ RuntimeError = Class.new(ExpressionError)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
new file mode 100644
index 00000000000..10957598f76
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Matches < Lexeme::Operator
+ PATTERN = /=~/.freeze
+
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def evaluate(variables = {})
+ text = @left.evaluate(variables)
+ regexp = @right.evaluate(variables)
+
+ regexp.scan(text.to_s).any?
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
new file mode 100644
index 00000000000..9b239c29ea4
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ require_dependency 're2'
+
+ class Pattern < Lexeme::Value
+ PATTERN = %r{^/.+/[ismU]*$}.freeze
+
+ def initialize(regexp)
+ @value = regexp
+
+ unless Gitlab::UntrustedRegexp.valid?(@value)
+ raise Lexer::SyntaxError, 'Invalid regular expression!'
+ end
+ end
+
+ def evaluate(variables = {})
+ Gitlab::UntrustedRegexp.fabricate(@value)
+ rescue RegexpError
+ raise Expression::RuntimeError, 'Invalid regular expression!'
+ end
+
+ def self.build(string)
+ new(string)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index e1c68b7c3c2..4cacb1e62c9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -5,15 +5,17 @@ module Gitlab
class Lexer
include ::Gitlab::Utils::StrongMemoize
+ SyntaxError = Class.new(Expression::ExpressionError)
+
LEXEMES = [
Expression::Lexeme::Variable,
Expression::Lexeme::String,
+ Expression::Lexeme::Pattern,
Expression::Lexeme::Null,
- Expression::Lexeme::Equals
+ Expression::Lexeme::Equals,
+ Expression::Lexeme::Matches
].freeze
- SyntaxError = Class.new(Statement::StatementError)
-
MAX_TOKENS = 100
def initialize(statement, max_tokens: MAX_TOKENS)
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index 09a7c98464b..b36f1e0f865 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -3,15 +3,16 @@ module Gitlab
module Pipeline
module Expression
class Statement
- StatementError = Class.new(StandardError)
+ StatementError = Class.new(Expression::ExpressionError)
GRAMMAR = [
+ %w[variable],
%w[variable equals string],
%w[variable equals variable],
%w[variable equals null],
%w[string equals variable],
%w[null equals variable],
- %w[variable]
+ %w[variable matches pattern]
].freeze
def initialize(statement, variables = {})
@@ -35,11 +36,13 @@ module Gitlab
def truthful?
evaluate.present?
+ rescue Expression::ExpressionError
+ false
end
def valid?
parse_tree.is_a?(Lexeme::Base)
- rescue StatementError
+ rescue Expression::ExpressionError
false
end
end
diff --git a/lib/gitlab/ci/pipeline/preloader.rb b/lib/gitlab/ci/pipeline/preloader.rb
new file mode 100644
index 00000000000..e7a2e5511cf
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/preloader.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ # Class for preloading data associated with pipelines such as commit
+ # authors.
+ module Preloader
+ def self.preload(pipelines)
+ # This ensures that all the pipeline commits are eager loaded before we
+ # start using them.
+ pipelines.each(&:commit)
+
+ pipelines.each do |pipeline|
+ # This preloads the author of every commit. We're using "lazy_author"
+ # here since "author" immediately loads the data on the first call.
+ pipeline.commit.try(:lazy_author)
+
+ # This preloads the number of warnings for every pipeline, ensuring
+ # that Ci::Pipeline#has_warnings? doesn't execute any additional
+ # queries.
+ pipeline.number_of_warnings
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index c101f30d6e8..2b58d9863a0 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -19,6 +19,7 @@ module Gitlab
def attributes
{ name: @attributes.fetch(:name),
+ position: @attributes.fetch(:index),
pipeline: @pipeline,
project: @pipeline.project }
end
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index 47b67930c6d..fe15fabc2e8 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -36,16 +36,16 @@ module Gitlab
end
def set(data)
- write do |stream|
+ write('w+b') do |stream|
data = job.hide_secrets(data)
stream.set(data)
end
end
def append(data, offset)
- write do |stream|
+ write('a+b') do |stream|
current_length = stream.size
- break -current_length unless current_length == offset
+ break current_length unless current_length == offset
data = job.hide_secrets(data)
stream.append(data, offset)
@@ -54,13 +54,15 @@ module Gitlab
end
def exist?
- trace_artifact&.exists? || current_path.present? || old_trace.present?
+ trace_artifact&.exists? || job.trace_chunks.any? || current_path.present? || old_trace.present?
end
def read
stream = Gitlab::Ci::Trace::Stream.new do
if trace_artifact
trace_artifact.open
+ elsif job.trace_chunks.any?
+ Gitlab::Ci::Trace::ChunkedIO.new(job)
elsif current_path
File.open(current_path, "rb")
elsif old_trace
@@ -73,9 +75,15 @@ module Gitlab
stream&.close
end
- def write
+ def write(mode)
stream = Gitlab::Ci::Trace::Stream.new do
- File.open(ensure_path, "a+b")
+ if current_path
+ File.open(current_path, mode)
+ elsif Feature.enabled?('ci_enable_live_trace')
+ Gitlab::Ci::Trace::ChunkedIO.new(job)
+ else
+ File.open(ensure_path, mode)
+ end
end
yield(stream).tap do
@@ -92,6 +100,7 @@ module Gitlab
FileUtils.rm(trace_path, force: true)
end
+ job.trace_chunks.fast_destroy_all
job.erase_old_trace!
end
@@ -99,7 +108,12 @@ module Gitlab
raise ArchiveError, 'Already archived' if trace_artifact
raise ArchiveError, 'Job is not finished yet' unless job.complete?
- if current_path
+ if job.trace_chunks.any?
+ Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream|
+ archive_stream!(stream)
+ stream.destroy!
+ end
+ elsif current_path
File.open(current_path) do |stream|
archive_stream!(stream)
FileUtils.rm(current_path)
@@ -116,7 +130,7 @@ module Gitlab
def archive_stream!(stream)
clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path|
- create_job_trace!(job, clone_path)
+ create_build_trace!(job, clone_path)
end
end
@@ -132,7 +146,7 @@ module Gitlab
end
end
- def create_job_trace!(job, path)
+ def create_build_trace!(job, path)
File.open(path) do |stream|
job.create_job_artifacts_trace!(
project: job.project,
diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb
new file mode 100644
index 00000000000..bfe0c2a2c26
--- /dev/null
+++ b/lib/gitlab/ci/trace/chunked_io.rb
@@ -0,0 +1,231 @@
+##
+# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
+# source: https://gitlab.com/snippets/1685610
+module Gitlab
+ module Ci
+ class Trace
+ class ChunkedIO
+ CHUNK_SIZE = ::Ci::BuildTraceChunk::CHUNK_SIZE
+
+ FailedToGetChunkError = Class.new(StandardError)
+
+ attr_reader :build
+ attr_reader :tell, :size
+ attr_reader :chunk, :chunk_range
+
+ alias_method :pos, :tell
+
+ def initialize(build, &block)
+ @build = build
+ @chunks_cache = []
+ @tell = 0
+ @size = calculate_size
+ yield self if block_given?
+ end
+
+ def close
+ # no-op
+ end
+
+ def binmode
+ # no-op
+ end
+
+ def binmode?
+ true
+ end
+
+ def seek(pos, where = IO::SEEK_SET)
+ new_pos =
+ case where
+ when IO::SEEK_END
+ size + pos
+ when IO::SEEK_SET
+ pos
+ when IO::SEEK_CUR
+ tell + pos
+ else
+ -1
+ end
+
+ raise ArgumentError, 'new position is outside of file' if new_pos < 0 || new_pos > size
+
+ @tell = new_pos
+ end
+
+ def eof?
+ tell == size
+ end
+
+ def each_line
+ until eof?
+ line = readline
+ break if line.nil?
+
+ yield(line)
+ end
+ end
+
+ def read(length = nil, outbuf = "")
+ out = ""
+
+ length ||= size - tell
+
+ until length <= 0 || eof?
+ data = chunk_slice_from_offset
+ break if data.empty?
+
+ chunk_bytes = [CHUNK_SIZE - chunk_offset, length].min
+ chunk_data = data.byteslice(0, chunk_bytes)
+
+ out << chunk_data
+ @tell += chunk_data.bytesize
+ length -= chunk_data.bytesize
+ end
+
+ # 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
+ end
+
+ out
+ end
+
+ def readline
+ out = ""
+
+ until eof?
+ data = chunk_slice_from_offset
+ new_line = data.index("\n")
+
+ if !new_line.nil?
+ out << data[0..new_line]
+ @tell += new_line + 1
+ break
+ else
+ out << data
+ @tell += data.bytesize
+ end
+ end
+
+ out
+ end
+
+ def write(data)
+ start_pos = tell
+
+ while tell < start_pos + data.bytesize
+ # get slice from current offset till the end where it falls into chunk
+ chunk_bytes = CHUNK_SIZE - chunk_offset
+ chunk_data = data.byteslice(tell - start_pos, chunk_bytes)
+
+ # append data to chunk, overwriting from that point
+ ensure_chunk.append(chunk_data, chunk_offset)
+
+ # move offsets within buffer
+ @tell += chunk_data.bytesize
+ @size = [size, tell].max
+ end
+
+ tell - start_pos
+ ensure
+ invalidate_chunk_cache
+ end
+
+ def truncate(offset)
+ raise ArgumentError, 'Outside of file' if offset > size || offset < 0
+ return if offset == size # Skip the following process as it doesn't affect anything
+
+ @tell = offset
+ @size = offset
+
+ # remove all next chunks
+ trace_chunks.where('chunk_index > ?', chunk_index).fast_destroy_all
+
+ # truncate current chunk
+ current_chunk.truncate(chunk_offset)
+ ensure
+ invalidate_chunk_cache
+ end
+
+ def flush
+ # no-op
+ end
+
+ def present?
+ true
+ end
+
+ def destroy!
+ trace_chunks.fast_destroy_all
+ @tell = @size = 0
+ ensure
+ invalidate_chunk_cache
+ end
+
+ private
+
+ ##
+ # The below methods are not implemented in IO class
+ #
+ def in_range?
+ @chunk_range&.include?(tell)
+ end
+
+ def chunk_slice_from_offset
+ unless in_range?
+ current_chunk.tap do |chunk|
+ raise FailedToGetChunkError unless chunk
+
+ @chunk = chunk.data
+ @chunk_range = chunk.range
+ end
+ end
+
+ @chunk[chunk_offset..CHUNK_SIZE]
+ end
+
+ def chunk_offset
+ tell % CHUNK_SIZE
+ end
+
+ def chunk_index
+ tell / CHUNK_SIZE
+ end
+
+ def chunk_start
+ chunk_index * CHUNK_SIZE
+ end
+
+ def chunk_end
+ [chunk_start + CHUNK_SIZE, size].min
+ end
+
+ def invalidate_chunk_cache
+ @chunks_cache = []
+ end
+
+ def current_chunk
+ @chunks_cache[chunk_index] ||= trace_chunks.find_by(chunk_index: chunk_index)
+ end
+
+ def build_chunk
+ @chunks_cache[chunk_index] = ::Ci::BuildTraceChunk.new(build: build, chunk_index: chunk_index)
+ end
+
+ def ensure_chunk
+ current_chunk || build_chunk
+ end
+
+ def trace_chunks
+ ::Ci::BuildTraceChunk.where(build: build)
+ end
+
+ def calculate_size
+ trace_chunks.order(chunk_index: :desc).first.try(&:end_offset).to_i
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 187ad8b833a..a71040e5e56 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -39,6 +39,8 @@ module Gitlab
end
def append(data, offset)
+ data = data.force_encoding(Encoding::BINARY)
+
stream.truncate(offset)
stream.seek(0, IO::SEEK_END)
stream.write(data)
@@ -46,8 +48,11 @@ module Gitlab
end
def set(data)
- truncate(0)
+ data = data.force_encoding(Encoding::BINARY)
+
+ stream.seek(0, IO::SEEK_SET)
stream.write(data)
+ stream.truncate(data.bytesize)
stream.flush()
end
@@ -127,11 +132,11 @@ module Gitlab
buf += debris
debris, *lines = buf.each_line.to_a
lines.reverse_each do |line|
- yield(line.force_encoding('UTF-8'))
+ yield(line.force_encoding(Encoding.default_external))
end
end
- yield(debris.force_encoding('UTF-8')) unless debris.empty?
+ yield(debris.force_encoding(Encoding.default_external)) unless debris.empty?
end
def read_backward(length)
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index e392a015b91..6cf7aa1bf0d 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -9,8 +9,8 @@ module Gitlab
end
end
- def fake_application_settings(defaults = ::ApplicationSetting.defaults)
- Gitlab::FakeApplicationSettings.new(defaults)
+ def fake_application_settings(attributes = {})
+ Gitlab::FakeApplicationSettings.new(::ApplicationSetting.defaults.merge(attributes || {}))
end
def method_missing(name, *args, &block)
@@ -25,43 +25,35 @@ module Gitlab
def ensure_application_settings!
return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
-
- cached_application_settings || uncached_application_settings
- end
-
- def cached_application_settings
- begin
- ::ApplicationSetting.cached
- rescue ::Redis::BaseError, ::Errno::ENOENT, ::Errno::EADDRNOTAVAIL
- # In case Redis isn't running or the Redis UNIX socket file is not available
- end
- end
-
- def uncached_application_settings
return fake_application_settings unless connect_to_db?
- db_settings = ::ApplicationSetting.current
-
+ current_settings = ::ApplicationSetting.current
# If there are pending migrations, it's possible there are columns that
# need to be added to the application settings. To prevent Rake tasks
# and other callers from failing, use any loaded settings and return
# defaults for missing columns.
if ActiveRecord::Migrator.needs_migration?
- defaults = ::ApplicationSetting.defaults
- defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present?
- return fake_application_settings(defaults)
+ return fake_application_settings(current_settings&.attributes)
end
- return db_settings if db_settings.present?
+ return current_settings if current_settings.present?
- ::ApplicationSetting.create_from_defaults || in_memory_application_settings
+ with_fallback_to_fake_application_settings do
+ ::ApplicationSetting.create_from_defaults || in_memory_application_settings
+ end
end
def in_memory_application_settings
- @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
- # In case migrations the application_settings table is not created yet,
- # we fallback to a simple OpenStruct
+ with_fallback_to_fake_application_settings do
+ @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+ end
+
+ def with_fallback_to_fake_application_settings(&block)
+ yield
+ rescue
+ # In case the application_settings table is not created yet, or if a new
+ # ApplicationSetting column is not yet migrated we fallback to a simple OpenStruct
fake_application_settings
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 76501dd50e8..d49d055c3f2 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -43,7 +43,7 @@ module Gitlab
end
def self.version
- database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
+ @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
def self.join_lateral_supported?
diff --git a/lib/gitlab/database/arel_methods.rb b/lib/gitlab/database/arel_methods.rb
new file mode 100644
index 00000000000..d7e3ce08b32
--- /dev/null
+++ b/lib/gitlab/database/arel_methods.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Database
+ module ArelMethods
+ private
+
+ # In Arel 7.0.0 (Arel 7.1.4 is used in Rails 5.0) the `engine` parameter of `Arel::UpdateManager#initializer`
+ # was removed.
+ # Remove this file and inline this method when removing rails5? code.
+ def arel_update_manager
+ if Gitlab.rails5?
+ Arel::UpdateManager.new
+ else
+ Arel::UpdateManager.new(ActiveRecord::Base)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
new file mode 100644
index 00000000000..5f549ed2b3c
--- /dev/null
+++ b/lib/gitlab/database/count.rb
@@ -0,0 +1,86 @@
+# 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
+ module Database
+ module Count
+ CONNECTION_ERRORS =
+ if defined?(PG)
+ [
+ ActionView::Template::Error,
+ ActiveRecord::StatementInvalid,
+ PG::Error
+ ].freeze
+ else
+ [
+ ActionView::Template::Error,
+ ActiveRecord::StatementInvalid
+ ].freeze
+ end
+
+ # Takes in an array of models and returns a Hash for the approximate
+ # counts for them. If the model's table has not been vacuumed or
+ # analyzed recently, simply run the Model.count to get the data.
+ #
+ # @param [Array]
+ # @return [Hash] of Model -> count mapping
+ def self.approximate_counts(models)
+ table_to_model_map = models.each_with_object({}) do |model, hash|
+ hash[model.table_name] = model
+ end
+
+ table_names = table_to_model_map.keys
+ counts_by_table_name = Gitlab::Database.postgresql? ? reltuples_from_recently_updated(table_names) : {}
+
+ # Convert table -> count to Model -> count
+ counts_by_model = counts_by_table_name.each_with_object({}) do |pair, hash|
+ model = table_to_model_map[pair.first]
+ hash[model] = pair.second
+ end
+
+ missing_tables = table_names - counts_by_table_name.keys
+
+ missing_tables.each do |table|
+ model = table_to_model_map[table]
+ counts_by_model[model] = model.count
+ end
+
+ counts_by_model
+ end
+
+ # Returns a hash of the table names that have recently updated tuples.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def self.reltuples_from_recently_updated(table_names)
+ query = postgresql_estimate_query(table_names)
+ rows = []
+
+ # Querying tuple stats only works on the primary. Due to load
+ # balancing, we need to ensure this query hits the load balancer. The
+ # easiest way to do this is to start a transaction.
+ ActiveRecord::Base.transaction do
+ rows = ActiveRecord::Base.connection.select_all(query)
+ end
+
+ rows.each_with_object({}) { |row, data| data[row['table_name']] = row['estimate'].to_i }
+ rescue *CONNECTION_ERRORS
+ {}
+ end
+
+ # Generates the PostgreSQL query to return the tuples for tables
+ # that have been vacuumed or analyzed in the last hour.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def self.postgresql_estimate_query(table_names)
+ time = "to_timestamp(#{1.hour.ago.to_i})"
+ <<~SQL
+ SELECT pg_class.relname AS table_name, reltuples::bigint AS estimate FROM pg_class
+ LEFT JOIN pg_stat_user_tables ON pg_class.relname = pg_stat_user_tables.relname
+ WHERE pg_class.relname IN (#{table_names.map { |table| "'#{table}'" }.join(',')})
+ AND (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 77079e5e72b..c21bae5e16b 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1,6 +1,8 @@
module Gitlab
module Database
module MigrationHelpers
+ include Gitlab::Database::ArelMethods
+
BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
@@ -314,7 +316,7 @@ module Gitlab
stop_arel = yield table, stop_arel if block_given?
stop_row = exec_query(stop_arel.to_sql).to_hash.first
- update_arel = Arel::UpdateManager.new(ActiveRecord::Base)
+ update_arel = arel_update_manager
.table(table)
.set([[table[column], value]])
.where(table[:id].gteq(start_id))
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 1a697396ff1..14de28a1d08 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
@@ -3,6 +3,8 @@ module Gitlab
module RenameReservedPathsMigration
module V1
class RenameBase
+ include Gitlab::Database::ArelMethods
+
attr_reader :paths, :migration
delegate :update_column_in_batches,
@@ -62,10 +64,10 @@ module Gitlab
old_full_path,
new_full_path)
- update = Arel::UpdateManager.new(ActiveRecord::Base)
- .table(routes)
- .set([[routes[:path], replace_statement]])
- .where(Arel::Nodes::SqlLiteral.new(filter))
+ update = arel_update_manager
+ .table(routes)
+ .set([[routes[:path], replace_statement]])
+ .where(Arel::Nodes::SqlLiteral.new(filter))
execute(update.to_sql)
end
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 05b86f32ce2..73971af6a74 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
@@ -62,21 +62,20 @@ module Gitlab
end
def move_repositories(namespace, old_full_path, new_full_path)
- repo_paths_for_namespace(namespace).each do |repository_storage_path|
+ repo_shards_for_namespace(namespace).each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, old_full_path)
+ gitlab_shell.add_namespace(repository_storage, old_full_path)
- unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path)
- message = "Exception moving path #{repository_storage_path} \
- from #{old_full_path} to #{new_full_path}"
+ unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
+ message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
Rails.logger.error message
end
end
end
- def repo_paths_for_namespace(namespace)
+ def repo_shards_for_namespace(namespace)
projects_for_namespace(namespace).distinct.select(:repository_storage)
- .map(&:repository_storage_path)
+ .map(&:repository_storage)
end
def projects_for_namespace(namespace)
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 979225dd216..827aeb12a02 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
@@ -51,7 +51,7 @@ module Gitlab
end
def move_repository(project, old_path, new_path)
- unless gitlab_shell.mv_repository(project.repository_storage_path,
+ unless gitlab_shell.mv_repository(project.repository_storage,
old_path,
new_path)
Rails.logger.error "Error moving #{old_path} to #{new_path}"
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 014854da55c..765fb0289a8 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -76,6 +76,13 @@ module Gitlab
line_code(line) if line
end
+ # Returns the raw diff content up to the given line index
+ def diff_hunk(diff_line)
+ # Adding 2 because of the @@ diff header and Enum#take should consider
+ # an extra line, because we're passing an index.
+ raw_diff.each_line.take(diff_line.index + 2).join
+ end
+
def old_sha
diff_refs&.base_sha
end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index a6007ebf531..c79d8d3cb21 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -36,6 +36,8 @@ module Gitlab
private
def decorate_diff!(diff)
+ return diff if diff.is_a?(File)
+
Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs, fallback_diff_refs: fallback_diff_refs)
end
end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 690b27cde81..978962ab2eb 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -12,6 +12,10 @@ module Gitlab
:head_sha,
:old_line,
:new_line,
+ :width,
+ :height,
+ :x,
+ :y,
:position_type, to: :formatter
# A position can belong to a text line or to an image coordinate
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 05a60deb7d3..764f93f6d3d 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -47,7 +47,7 @@ module Gitlab
project,
author,
title: mail.subject,
- description: message
+ description: message_including_reply
).execute
end
end
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 8eea33b9ab5..5791dbd0484 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -8,6 +8,7 @@ module Gitlab
include ReplyProcessing
delegate :project, to: :sent_notification, allow_nil: true
+ delegate :noteable, to: :sent_notification
def can_handle?
mail_key =~ /\A\w+\z/
@@ -18,7 +19,7 @@ module Gitlab
validate_permission!(:create_note)
- raise NoteableNotFoundError unless sent_notification.noteable
+ raise NoteableNotFoundError unless noteable
raise EmptyEmailError if message.blank?
verify_record!(
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
index 32c5caf93e8..38b1425364f 100644
--- a/lib/gitlab/email/handler/reply_processing.rb
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -16,8 +16,12 @@ module Gitlab
@message ||= process_message
end
- def process_message
- message = ReplyParser.new(mail).execute.strip
+ def message_including_reply
+ @message_with_reply ||= process_message(trim_reply: false)
+ end
+
+ def process_message(**kwargs)
+ message = ReplyParser.new(mail, **kwargs).execute.strip
add_attachments(message)
end
@@ -32,8 +36,12 @@ module Gitlab
def validate_permission!(permission)
raise UserNotFoundError unless author
raise UserBlockedError if author.blocked?
- raise ProjectNotFound unless author.can?(:read_project, project)
- raise UserNotAuthorizedError unless author.can?(permission, project)
+
+ if project
+ raise ProjectNotFound unless author.can?(:read_project, project)
+ end
+
+ raise UserNotAuthorizedError unless author.can?(permission, project || noteable)
end
def verify_record!(record:, invalid_exception:, record_name:)
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 01c28d051ee..ae6b84607d6 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -4,8 +4,9 @@ module Gitlab
class ReplyParser
attr_accessor :message
- def initialize(message)
+ def initialize(message, trim_reply: true)
@message = message
+ @trim_reply = trim_reply
end
def execute
@@ -13,7 +14,9 @@ module Gitlab
encoding = body.encoding
- body = EmailReplyTrimmer.trim(body)
+ if @trim_reply
+ body = EmailReplyTrimmer.trim(body)
+ end
return '' unless body
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index cc2638172ec..49bc9c0b671 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -14,6 +14,7 @@ module Gitlab
avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
+ xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z},
# Configuration files
gitignore: '.gitignore',
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index c9abea90d21..e85e87a54af 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -1,3 +1,5 @@
+require_dependency 'gitlab/encoding_helper'
+
module Gitlab
module Git
# The ID of empty tree.
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 6d6ed065f79..4158d50cd9e 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -15,10 +15,7 @@ module Gitlab
def each
@blames.each do |blame|
- yield(
- Gitlab::Git::Commit.new(@repo, blame.commit),
- blame.line
- )
+ yield(blame.commit, blame.line)
end
end
@@ -60,9 +57,8 @@ module Gitlab
end
end
- # load all commits in single call
- commits.keys.each do |key|
- commits[key] = @repo.lookup(key)
+ Gitlab::Git::Commit.batch_by_oid(@repo, commits.keys).each do |commit|
+ commits[commit.sha] = commit
end
# get it together
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index eabcf46cf58..156d077a69c 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -62,6 +62,12 @@ module Gitlab
end
end
+ # Returns an array of Blob instances just with the metadata, that means
+ # the data attribute has no content.
+ def batch_metadata(repository, blob_references)
+ batch(repository, blob_references, blob_size_limit: 0)
+ end
+
# Find LFS blobs given an array of sha ids
# Returns array of Gitlab::Git::Blob
# Does not guarantee blob data will be set
@@ -98,25 +104,22 @@ module Gitlab
# file.rb # oid: 4a
#
#
- # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a'
+ # Blob.find_entry_by_path(repo, '1a', 'blog', 'app', 'file.rb') # => '4a'
#
- def find_entry_by_path(repository, root_id, path)
+ def find_entry_by_path(repository, root_id, *path_parts)
root_tree = repository.lookup(root_id)
- # Strip leading slashes
- path[%r{^/*}] = ''
- path_arr = path.split('/')
entry = root_tree.find do |entry|
- entry[:name] == path_arr[0]
+ entry[:name] == path_parts[0]
end
return nil unless entry
- if path_arr.size > 1
+ if path_parts.size > 1
return nil unless entry[:type] == :tree
- path_arr.shift
- find_entry_by_path(repository, entry[:oid], path_arr.join('/'))
+ path_parts.shift
+ find_entry_by_path(repository, entry[:oid], *path_parts)
else
[:blob, :commit].include?(entry[:type]) ? entry : nil
end
@@ -179,10 +182,13 @@ module Gitlab
def find_by_rugged(repository, sha, path, limit:)
return unless path
+ # Strip any leading / characters from the path
+ path = path.sub(%r{\A/*}, '')
+
rugged_commit = repository.lookup(sha)
root_tree = rugged_commit.tree
- blob_entry = find_entry_by_path(repository, root_tree.oid, path)
+ blob_entry = find_entry_by_path(repository, root_tree.oid, *path.split('/'))
return nil unless blob_entry
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index fabcd46c8e9..89f761dd515 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -6,6 +6,7 @@ module Gitlab
attr_accessor :raw_commit, :head
+ MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes
MIN_SHA_LENGTH = 7
SERIALIZE_KEYS = [
:id, :message, :parent_ids,
@@ -63,9 +64,7 @@ module Gitlab
if is_enabled
repo.gitaly_commit_client.find_commit(commit_id)
else
- obj = repo.rev_parse_target(commit_id)
-
- obj.is_a?(Rugged::Commit) ? obj : nil
+ rugged_find(repo, commit_id)
end
end
@@ -76,6 +75,12 @@ module Gitlab
nil
end
+ def rugged_find(repo, commit_id)
+ obj = repo.rev_parse_target(commit_id)
+
+ obj.is_a?(Rugged::Commit) ? obj : nil
+ end
+
# Get last commit for HEAD
#
# Ex.
@@ -297,11 +302,40 @@ module Gitlab
nil
end
end
+
+ def get_message(repository, commit_id)
+ BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader|
+ items_by_repo = items.group_by { |i| i[:repository] }
+
+ items_by_repo.each do |repo, items|
+ commit_ids = items.map { |i| i[:commit_id] }
+
+ messages = get_messages(repository, commit_ids)
+
+ messages.each do |commit_sha, message|
+ loader.call({ repository: repository, commit_id: commit_sha }, message)
+ end
+ end
+ end
+ end
+
+ def get_messages(repository, commit_ids)
+ repository.gitaly_migrate(:commit_messages) do |is_enabled|
+ if is_enabled
+ repository.gitaly_commit_client.get_commit_messages(commit_ids)
+ else
+ commit_ids.map { |id| [id, rugged_find(repository, id).message] }.to_h
+ end
+ end
+ end
end
def initialize(repository, raw_commit, head = nil)
raise "Nil as raw commit passed" unless raw_commit
+ @repository = repository
+ @head = head
+
case raw_commit
when Hash
init_from_hash(raw_commit)
@@ -312,9 +346,6 @@ module Gitlab
else
raise "Invalid raw commit type: #{raw_commit.class}"
end
-
- @repository = repository
- @head = head
end
def sha
@@ -342,21 +373,6 @@ module Gitlab
parent_ids.first
end
- # Shows the diff between the commit's parent and the commit.
- #
- # Cuts out the header and stats from #to_patch and returns only the diff.
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
- def to_diff
- Gitlab::GitalyClient.migrate(:commit_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- @repository.gitaly_commit_client.patch(id)
- else
- rugged_diff_from_parent.patch
- end
- end
- end
-
# Returns a diff object for the changes from this commit's first parent.
# If there is no parent, then the diff is between this commit and an
# empty repo. See Repository#diff for keys allowed in the +options+
@@ -432,16 +448,6 @@ module Gitlab
Gitlab::Git::CommitStats.new(@repository, self)
end
- def to_patch(options = {})
- begin
- rugged_commit.to_mbox(options)
- rescue Rugged::InvalidError => ex
- if ex.message =~ /commit \w+ is a merge commit/i
- 'Patch format is not currently supported for merge commits.'
- end
- end
- end
-
# Get ref names collection
#
# Ex.
@@ -543,7 +549,7 @@ module Gitlab
# TODO: Once gitaly "takes over" Rugged consider separating the
# subject from the message to make it clearer when there's one
# available but not the other.
- @message = (commit.body.presence || commit.subject).dup
+ @message = message_from_gitaly_body
@authored_date = Time.at(commit.author.date.seconds).utc
@author_name = commit.author.name.dup
@author_email = commit.author.email.dup
@@ -595,6 +601,25 @@ module Gitlab
def refs(repo)
repo.refs_hash[id]
end
+
+ def message_from_gitaly_body
+ return @raw_commit.subject.dup if @raw_commit.body_size.zero?
+ return @raw_commit.body.dup if full_body_fetched_from_gitaly?
+
+ if @raw_commit.body_size > MAX_COMMIT_MESSAGE_DISPLAY_SIZE
+ "#{@raw_commit.subject}\n\n--commit message is too big".strip
+ else
+ fetch_body_from_gitaly
+ end
+ end
+
+ def full_body_fetched_from_gitaly?
+ @raw_commit.body.bytesize == @raw_commit.body_size
+ end
+
+ def fetch_body_from_gitaly
+ self.class.get_message(@repository, id)
+ end
end
end
end
diff --git a/lib/gitlab/git/committer_with_hooks.rb b/lib/gitlab/git/committer_with_hooks.rb
new file mode 100644
index 00000000000..a8a59f998cd
--- /dev/null
+++ b/lib/gitlab/git/committer_with_hooks.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Git
+ class CommitterWithHooks < Gollum::Committer
+ attr_reader :gl_wiki
+
+ def initialize(gl_wiki, options = {})
+ @gl_wiki = gl_wiki
+ super(gl_wiki.gollum_wiki, options)
+ end
+
+ def commit
+ # TODO: Remove after 10.8
+ return super unless allowed_to_run_hooks?
+
+ result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
+ @wiki.ref,
+ start_branch_name: @wiki.ref
+ ) do |start_commit|
+ super(false)
+ end
+
+ result[:newrev]
+ rescue Gitlab::Git::HooksService::PreReceiveError => e
+ message = "Custom Hook failed: #{e.message}"
+ raise Gitlab::Git::Wiki::OperationError, message
+ end
+
+ private
+
+ # TODO: Remove after 10.8
+ def allowed_to_run_hooks?
+ @options[:user_id] != 0 && @options[:username].present?
+ end
+
+ def git_user
+ @git_user ||= Gitlab::Git::User.new(@options[:username],
+ @options[:name],
+ @options[:email],
+ gitlab_id)
+ end
+
+ def gitlab_id
+ Gitlab::GlId.gl_id_from_id_value(@options[:user_id])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
index 099709620b3..00c943fdb25 100644
--- a/lib/gitlab/git/gitlab_projects.rb
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -53,7 +53,7 @@ module Gitlab
# Import project via git clone --bare
# URL must be publicly cloneable
def import_project(source, timeout)
- Gitlab::GitalyClient.migrate(:import_repository) do |is_enabled|
+ Gitlab::GitalyClient.migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_import_repository(source)
else
@@ -63,7 +63,8 @@ module Gitlab
end
def fork_repository(new_shard_name, new_repository_relative_path)
- Gitlab::GitalyClient.migrate(:fork_repository) do |is_enabled|
+ Gitlab::GitalyClient.migrate(:fork_repository,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_fork_repository(new_shard_name, new_repository_relative_path)
else
diff --git a/lib/gitlab/git/path_helper.rb b/lib/gitlab/git/path_helper.rb
index 155cf52f050..57b82a37d6c 100644
--- a/lib/gitlab/git/path_helper.rb
+++ b/lib/gitlab/git/path_helper.rb
@@ -6,7 +6,7 @@ module Gitlab
class << self
def normalize_path(filename)
# Strip all leading slashes so that //foo -> foo
- filename[%r{^/*}] = ''
+ filename = filename.sub(%r{\A/*}, '')
# Expand relative paths (e.g. foo/../bar)
filename = Pathname.new(filename)
diff --git a/lib/gitlab/git/raw_diff_change.rb b/lib/gitlab/git/raw_diff_change.rb
index eb3d8819239..98de9328071 100644
--- a/lib/gitlab/git/raw_diff_change.rb
+++ b/lib/gitlab/git/raw_diff_change.rb
@@ -6,7 +6,15 @@ module Gitlab
attr_reader :blob_id, :blob_size, :old_path, :new_path, :operation
def initialize(raw_change)
- parse(raw_change)
+ if raw_change.is_a?(Gitaly::GetRawChangesResponse::RawChange)
+ @blob_id = raw_change.blob_id
+ @blob_size = raw_change.size
+ @old_path = raw_change.old_path.presence
+ @new_path = raw_change.new_path.presence
+ @operation = raw_change.operation&.downcase || :unknown
+ else
+ parse(raw_change)
+ end
end
private
@@ -20,13 +28,14 @@ module Gitlab
# 85bc2f9753afd5f4fc5d7c75f74f8d526f26b4f3 107 R060\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee
def parse(raw_change)
@blob_id, @blob_size, @raw_operation, raw_paths = raw_change.split(' ', 4)
+ @blob_size = @blob_size.to_i
@operation = extract_operation
@old_path, @new_path = extract_paths(raw_paths)
end
def extract_paths(file_path)
case operation
- when :renamed
+ when :copied, :renamed
file_path.split(/\t/)
when :deleted
[file_path, nil]
@@ -38,7 +47,9 @@ module Gitlab
end
def extract_operation
- case @raw_operation&.first(1)
+ return :unknown unless @raw_operation
+
+ case @raw_operation[0]
when 'A'
:added
when 'C'
diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb
index 6bd6e58feeb..f40e59a8dd0 100644
--- a/lib/gitlab/git/remote_repository.rb
+++ b/lib/gitlab/git/remote_repository.rb
@@ -12,7 +12,7 @@ module Gitlab
# class.
#
class RemoteRepository
- attr_reader :path, :relative_path, :gitaly_repository
+ attr_reader :relative_path, :gitaly_repository
def initialize(repository)
@relative_path = repository.relative_path
@@ -21,7 +21,6 @@ module Gitlab
# These instance variables will not be available in gitaly-ruby, where
# we have no disk access to this repository.
@repository = repository
- @path = repository.path
end
def empty?
@@ -69,6 +68,10 @@ module Gitlab
env
end
+ def path
+ @repository.path
+ end
+
private
# Must return an object that responds to 'address' and 'storage'.
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 3124c426f97..1a21625a322 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -20,6 +20,9 @@ module Gitlab
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
].freeze
SEARCH_CONTEXT_LINES = 3
+ # In https://gitlab.com/gitlab-org/gitaly/merge_requests/698
+ # We copied these two prefixes into gitaly-go, so don't change these
+ # or things will break! (REBASE_WORKTREE_PREFIX and SQUASH_WORKTREE_PREFIX)
REBASE_WORKTREE_PREFIX = 'rebase'.freeze
SQUASH_WORKTREE_PREFIX = 'squash'.freeze
GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
@@ -27,6 +30,7 @@ module Gitlab
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze
NoRepository = Class.new(StandardError)
+ InvalidRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError)
InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError)
@@ -142,15 +146,7 @@ module Gitlab
end
def exists?
- Gitlab::GitalyClient.migrate(:repository_exists, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- gitaly_repository_client.exists?
- else
- circuit_breaker.perform do
- File.exist?(File.join(path, 'refs'))
- end
- end
- end
+ gitaly_repository_client.exists?
end
# Returns an Array of branch names
@@ -399,18 +395,6 @@ module Gitlab
nil
end
- def archive_prefix(ref, sha, append_sha:)
- append_sha = (ref != sha) if append_sha.nil?
-
- project_name = self.name.chomp('.git')
- formatted_ref = ref.tr('/', '-')
-
- prefix_segments = [project_name, formatted_ref]
- prefix_segments << sha if append_sha
-
- prefix_segments.join('-')
- end
-
def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
@@ -419,14 +403,46 @@ module Gitlab
prefix = archive_prefix(ref, commit.id, append_sha: append_sha)
{
- 'RepoPath' => path,
'ArchivePrefix' => prefix,
- 'ArchivePath' => archive_file_path(prefix, storage_path, format),
- 'CommitId' => commit.id
+ 'ArchivePath' => archive_file_path(storage_path, commit.id, prefix, format),
+ 'CommitId' => commit.id,
+ 'GitalyRepository' => gitaly_repository.to_h
}
end
- def archive_file_path(name, storage_path, format = "tar.gz")
+ # This is both the filename of the archive (missing the extension) and the
+ # name of the top-level member of the archive under which all files go
+ #
+ # FIXME: The generated prefix is incorrect for projects with hashed
+ # storage enabled
+ def archive_prefix(ref, sha, append_sha:)
+ append_sha = (ref != sha) if append_sha.nil?
+
+ project_name = self.name.chomp('.git')
+ formatted_ref = ref.tr('/', '-')
+
+ prefix_segments = [project_name, formatted_ref]
+ prefix_segments << sha if append_sha
+
+ prefix_segments.join('-')
+ end
+ private :archive_prefix
+
+ # The full path on disk where the archive should be stored. This is used
+ # to cache the archive between requests.
+ #
+ # The path is a global namespace, so needs to be globally unique. This is
+ # achieved by including `gl_repository` in the path.
+ #
+ # Archives relating to a particular ref when the SHA is not present in the
+ # filename must be invalidated when the ref is updated to point to a new
+ # SHA. This is achieved by including the SHA in the path.
+ #
+ # As this is a full path on disk, it is not "cloud native". This should
+ # be resolved by either removing the cache, or moving the implementation
+ # into Gitaly and removing the ArchivePath parameter from the git-archive
+ # senddata response.
+ def archive_file_path(storage_path, sha, name, format = "tar.gz")
# Build file path
return nil unless name
@@ -444,8 +460,9 @@ module Gitlab
end
file_name = "#{name}.#{extension}"
- File.join(storage_path, self.name, file_name)
+ File.join(storage_path, self.gl_repository, sha, file_name)
end
+ private :archive_file_path
# Return repo size in megabytes
def size
@@ -565,19 +582,36 @@ module Gitlab
# old_rev and new_rev are commit ID's
# the result of this method is an array of Gitlab::Git::RawDiffChange
def raw_changes_between(old_rev, new_rev)
- result = []
+ @raw_changes_between ||= {}
- circuit_breaker.perform do
- Open3.pipeline_r(git_diff_cmd(old_rev, new_rev), format_git_cat_file_script, git_cat_file_cmd) do |last_stdout, wait_threads|
- last_stdout.each_line { |line| result << ::Gitlab::Git::RawDiffChange.new(line.chomp!) }
+ @raw_changes_between[[old_rev, new_rev]] ||= begin
+ return [] if new_rev.blank? || new_rev == Gitlab::Git::BLANK_SHA
+
+ gitaly_migrate(:raw_changes_between) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.raw_changes_between(old_rev, new_rev)
+ .each_with_object([]) do |msg, arr|
+ msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) }
+ end
+ else
+ result = []
- if wait_threads.any? { |waiter| !waiter.value&.success? }
- raise ::Gitlab::Git::Repository::GitError, "Unabled to obtain changes between #{old_rev} and #{new_rev}"
+ circuit_breaker.perform do
+ Open3.pipeline_r(git_diff_cmd(old_rev, new_rev), format_git_cat_file_script, git_cat_file_cmd) do |last_stdout, wait_threads|
+ last_stdout.each_line { |line| result << ::Gitlab::Git::RawDiffChange.new(line.chomp!) }
+
+ if wait_threads.any? { |waiter| !waiter.value&.success? }
+ raise ::Gitlab::Git::Repository::GitError, "Unabled to obtain changes between #{old_rev} and #{new_rev}"
+ end
+ end
+ end
+
+ result
end
end
end
-
- result
+ rescue ArgumentError => e
+ raise Gitlab::Git::Repository::GitError.new(e)
end
# Returns the SHA of the most recent common ancestor of +from+ and +to+
@@ -742,13 +776,9 @@ module Gitlab
end
def add_branch(branch_name, user:, target:)
- gitaly_migrate(:operation_user_create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_add_branch(branch_name, user, target)
- else
- rugged_add_branch(branch_name, user, target)
- end
- end
+ gitaly_operation_client.user_create_branch(branch_name, user, target)
+ rescue GRPC::FailedPrecondition => ex
+ raise InvalidRef, ex
end
def add_tag(tag_name, user:, target:, message: nil)
@@ -1018,7 +1048,7 @@ module Gitlab
return @info_attributes if @info_attributes
content =
- gitaly_migrate(:get_info_attributes) do |is_enabled|
+ gitaly_migrate(:get_info_attributes, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.info_attributes
else
@@ -1187,6 +1217,8 @@ module Gitlab
if is_enabled
gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref)
else
+ # When removing this code, also remove source_repository#path
+ # to remove deprecated method calls
local_fetch_ref(source_repository.path, source_ref: source_ref, target_ref: target_ref)
end
end
@@ -1258,6 +1290,10 @@ module Gitlab
true
end
+ def create_from_snapshot(url, auth)
+ gitaly_repository_client.create_from_snapshot(url, auth)
+ end
+
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
gitaly_migrate(:rebase) do |is_enabled|
if is_enabled
@@ -1298,7 +1334,7 @@ module Gitlab
end
def squash_in_progress?(squash_id)
- gitaly_migrate(:squash_in_progress) do |is_enabled|
+ gitaly_migrate(:squash_in_progress, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_repository_client.squash_in_progress?(squash_id)
else
@@ -1427,34 +1463,29 @@ module Gitlab
end
def branch_names_contains_sha(sha)
- gitaly_migrate(:branch_names_contains_sha,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_ref_client.branch_names_contains_sha(sha)
- else
- refs_contains_sha('refs/heads/', sha)
- end
- end
+ gitaly_ref_client.branch_names_contains_sha(sha)
end
def tag_names_contains_sha(sha)
- gitaly_migrate(:tag_names_contains_sha,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- gitaly_ref_client.tag_names_contains_sha(sha)
- else
- refs_contains_sha('refs/tags/', sha)
- end
- end
+ gitaly_ref_client.tag_names_contains_sha(sha)
end
def search_files_by_content(query, ref)
return [] if empty? || query.blank?
- offset = 2
- args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
+ safe_query = Regexp.escape(query)
+ ref ||= root_ref
+
+ gitaly_migrate(:search_files_by_content) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.search_files_by_content(ref, safe_query)
+ else
+ offset = 2
+ args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{safe_query} #{ref})
- run_git(args).first.scrub.split(/^--\n/)
+ run_git(args).first.scrub.split(/^--\n/)
+ end
+ end
end
def can_be_merged?(source_sha, target_branch)
@@ -1469,12 +1500,19 @@ module Gitlab
def search_files_by_name(query, ref)
safe_query = Regexp.escape(query.sub(%r{^/*}, ""))
+ ref ||= root_ref
return [] if empty? || safe_query.blank?
- args = %W(ls-tree -r --name-status --full-tree #{ref || root_ref} -- #{safe_query})
+ gitaly_migrate(:search_files_by_name) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.search_files_by_name(ref, safe_query)
+ else
+ args = %W(ls-tree -r --name-status --full-tree #{ref} -- #{safe_query})
- run_git(args).first.lines.map(&:strip)
+ run_git(args).first.lines.map(&:strip)
+ end
+ end
end
def find_commits_by_message(query, ref, path, limit, offset)
@@ -1550,13 +1588,12 @@ module Gitlab
end
def checksum
- gitaly_migrate(:calculate_checksum) do |is_enabled|
- if is_enabled
- gitaly_repository_client.calculate_checksum
- else
- calculate_checksum_by_shelling_out
- end
- end
+ # The exists? RPC is much cheaper, so we perform this request first
+ raise NoRepository, "Repository does not exists" unless exists?
+
+ gitaly_repository_client.calculate_checksum
+ rescue GRPC::NotFound
+ raise NoRepository # Guard against data races.
end
private
@@ -1579,27 +1616,6 @@ module Gitlab
end
end
- def refs_contains_sha(refs_prefix, sha)
- refs_prefix << "/" unless refs_prefix.ends_with?('/')
-
- # By forcing the output to %(refname) each line wiht a ref will start with
- # the ref prefix. All other lines can be discarded.
- args = %W(for-each-ref --contains=#{sha} --format=%(refname) #{refs_prefix})
- names, code = run_git(args)
-
- return [] unless code.zero?
-
- refs = []
- left_slice_count = refs_prefix.length
- names.lines.each do |line|
- next unless line.start_with?(refs_prefix)
-
- refs << encode_utf8(line.rstrip[left_slice_count..-1])
- end
-
- refs
- end
-
def rugged_write_config(full_path:)
rugged.config['gitlab.fullpath'] = full_path
end
@@ -1651,10 +1667,14 @@ module Gitlab
end
end
+ # This function is duplicated in Gitaly-Go, don't change it!
+ # https://gitlab.com/gitlab-org/gitaly/merge_requests/698
def fresh_worktree?(path)
File.exist?(path) && !clean_stuck_worktree(path)
end
+ # This function is duplicated in Gitaly-Go, don't change it!
+ # https://gitlab.com/gitlab-org/gitaly/merge_requests/698
def clean_stuck_worktree(path)
return false unless File.mtime(path) < 15.minutes.ago
@@ -1942,7 +1962,12 @@ module Gitlab
end
target_commit = Gitlab::Git::Commit.find(self, ref.target)
- Gitlab::Git::Tag.new(self, ref.name, ref.target, target_commit, message)
+ Gitlab::Git::Tag.new(self, {
+ name: ref.name,
+ target: ref.target,
+ target_commit: target_commit,
+ message: message
+ })
end.sort_by(&:name)
end
@@ -2187,22 +2212,6 @@ module Gitlab
end
end
- def gitaly_add_branch(branch_name, user, target)
- gitaly_operation_client.user_create_branch(branch_name, user, target)
- rescue GRPC::FailedPrecondition => ex
- raise InvalidRef, ex
- end
-
- def rugged_add_branch(branch_name, user, target)
- target_object = Ref.dereference_object(lookup(target))
- raise InvalidRef.new("target not found: #{target}") unless target_object
-
- OperationService.new(user, self).add_branch(branch_name, target_object.oid)
- find_branch(branch_name)
- rescue Rugged::ReferenceError => ex
- raise InvalidRef, ex
- end
-
def rugged_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch(
branch_name,
@@ -2358,7 +2367,7 @@ module Gitlab
end
def gitaly_delete_refs(*ref_names)
- gitaly_ref_client.delete_refs(refs: ref_names)
+ gitaly_ref_client.delete_refs(refs: ref_names) if ref_names.any?
end
def rugged_remove_remote(remote_name)
@@ -2488,34 +2497,6 @@ module Gitlab
rev_parse_target(ref).oid
end
- def calculate_checksum_by_shelling_out
- raise NoRepository unless exists?
-
- args = %W(--git-dir=#{path} show-ref --heads --tags)
- output, status = run_git(args)
-
- if status.nil? || !status.zero?
- # Empty repositories return with a non-zero status and an empty output.
- return EMPTY_REPOSITORY_CHECKSUM if output&.empty?
-
- raise ChecksumError, output
- end
-
- refs = output.split("\n")
-
- result = refs.inject(nil) do |checksum, ref|
- value = Digest::SHA1.hexdigest(ref).hex
-
- if checksum.nil?
- value
- else
- checksum ^ value
- end
- end
-
- result.to_s(16)
- end
-
def build_git_cmd(*args)
object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index 8a01f92e2af..e35ea5762eb 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -35,7 +35,11 @@ module Gitlab
next if name =~ /\^\{\}\Z/
target_commit = Gitlab::Git::Commit.find(self, target)
- Gitlab::Git::Tag.new(self, name, target, target_commit)
+ Gitlab::Git::Tag.new(self, {
+ name: name,
+ target: target,
+ target_commit: target_commit
+ })
end.compact
end
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index 8a8f7b051ed..e44284572fd 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -1,17 +1,99 @@
module Gitlab
module Git
class Tag < Ref
- attr_reader :object_sha
+ extend Gitlab::EncodingHelper
+
+ attr_reader :object_sha, :repository
+
+ MAX_TAG_MESSAGE_DISPLAY_SIZE = 10.megabytes
+ SERIALIZE_KEYS = %i[name target target_commit message].freeze
+
+ attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
+
+ class << self
+ def get_message(repository, tag_id)
+ BatchLoader.for({ repository: repository, tag_id: tag_id }).batch do |items, loader|
+ items_by_repo = items.group_by { |i| i[:repository] }
+
+ items_by_repo.each do |repo, items|
+ tag_ids = items.map { |i| i[:tag_id] }
+
+ messages = get_messages(repository, tag_ids)
+
+ messages.each do |id, message|
+ loader.call({ repository: repository, tag_id: id }, message)
+ end
+ end
+ end
+ end
+
+ def get_messages(repository, tag_ids)
+ repository.gitaly_migrate(:tag_messages) do |is_enabled|
+ if is_enabled
+ repository.gitaly_ref_client.get_tag_messages(tag_ids)
+ else
+ tag_ids.map do |id|
+ tag = repository.rugged.lookup(id)
+ message = tag.is_a?(Rugged::Commit) ? "" : tag.message
+
+ [id, message]
+ end.to_h
+ end
+ end
+ end
+ end
+
+ def initialize(repository, raw_tag)
+ @repository = repository
+ @raw_tag = raw_tag
+
+ case raw_tag
+ when Hash
+ init_from_hash
+ when Gitaly::Tag
+ init_from_gitaly
+ end
- def initialize(repository, name, target, target_commit, message = nil)
super(repository, name, target, target_commit)
+ end
+
+ def init_from_hash
+ raw_tag = @raw_tag.symbolize_keys
+
+ SERIALIZE_KEYS.each do |key|
+ send("#{key}=", raw_tag[key]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def init_from_gitaly
+ @name = encode!(@raw_tag.name.dup)
+ @target = @raw_tag.id
+ @message = message_from_gitaly_tag
- @message = message
+ if @raw_tag.target_commit.present?
+ @target_commit = Gitlab::Git::Commit.decorate(repository, @raw_tag.target_commit)
+ end
end
def message
encode! @message
end
+
+ private
+
+ def message_from_gitaly_tag
+ return @raw_tag.message.dup if full_message_fetched_from_gitaly?
+
+ if @raw_tag.message_size > MAX_TAG_MESSAGE_DISPLAY_SIZE
+ '--tag message is too big'
+ else
+ self.class.get_message(@repository, target)
+ end
+ end
+
+ def full_message_fetched_from_gitaly?
+ @raw_tag.message.bytesize == @raw_tag.message_size
+ end
end
end
end
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 8d82820915d..1ab8c4e0229 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -2,10 +2,11 @@ module Gitlab
module Git
class Wiki
DuplicatePageError = Class.new(StandardError)
+ OperationError = Class.new(StandardError)
- CommitDetails = Struct.new(:name, :email, :message) do
+ CommitDetails = Struct.new(:user_id, :username, :name, :email, :message) do
def to_h
- { name: name, email: email, message: message }
+ { user_id: user_id, username: username, name: name, email: email, message: message }
end
end
PageBlob = Struct.new(:name)
@@ -66,7 +67,8 @@ module Gitlab
end
def page(title:, version: nil, dir: nil)
- @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_find_page,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_find_page(title: title, version: version, dir: dir)
else
@@ -129,7 +131,7 @@ module Gitlab
def page_formatted_data(title:, dir: nil, version: nil)
version = version&.id
- @repository.gitaly_migrate(:wiki_page_formatted_data) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_page_formatted_data, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
else
@@ -140,6 +142,10 @@ module Gitlab
end
end
+ def gollum_wiki
+ @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
+ end
+
private
# options:
@@ -158,10 +164,6 @@ module Gitlab
offset: options[:offset])
end
- def gollum_wiki
- @gollum_wiki ||= Gollum::Wiki.new(@repository.path)
- end
-
def gollum_page_by_path(page_path)
page_name = Gollum::Page.canonicalize_filename(page_path)
page_dir = File.split(page_path).first
@@ -201,12 +203,12 @@ module Gitlab
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- filename = File.basename(name)
- dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
-
- gollum_wiki.write_page(filename, format, content, commit_details.to_h, dir)
+ with_committer_with_hooks(commit_details) do |committer|
+ filename = File.basename(name)
+ dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir
- nil
+ gollum_wiki.write_page(filename, format, content, { committer: committer }, dir)
+ end
rescue Gollum::DuplicatePageError => e
raise Gitlab::Git::Wiki::DuplicatePageError, e.message
end
@@ -214,24 +216,23 @@ module Gitlab
def gollum_delete_page(page_path, commit_details)
assert_type!(commit_details, CommitDetails)
- gollum_wiki.delete_page(gollum_page_by_path(page_path), commit_details.to_h)
- nil
+ with_committer_with_hooks(commit_details) do |committer|
+ gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer)
+ end
end
def gollum_update_page(page_path, title, format, content, commit_details)
assert_type!(format, Symbol)
assert_type!(commit_details, CommitDetails)
- page = gollum_page_by_path(page_path)
- committer = Gollum::Committer.new(page.wiki, commit_details.to_h)
-
- # Instead of performing two renames if the title has changed,
- # the update_page will only update the format and content and
- # the rename_page will do anything related to moving/renaming
- gollum_wiki.update_page(page, page.name, format, content, committer: committer)
- gollum_wiki.rename_page(page, title, committer: committer)
- committer.commit
- nil
+ with_committer_with_hooks(commit_details) do |committer|
+ page = gollum_page_by_path(page_path)
+ # Instead of performing two renames if the title has changed,
+ # the update_page will only update the format and content and
+ # the rename_page will do anything related to moving/renaming
+ gollum_wiki.update_page(page, page.name, format, content, committer: committer)
+ gollum_wiki.rename_page(page, title, committer: committer)
+ end
end
def gollum_find_page(title:, version: nil, dir: nil)
@@ -288,6 +289,20 @@ module Gitlab
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
+
+ def committer_with_hooks(commit_details)
+ Gitlab::Git::CommitterWithHooks.new(self, commit_details.to_h)
+ end
+
+ def with_committer_with_hooks(commit_details, &block)
+ committer = committer_with_hooks(commit_details)
+
+ yield committer
+
+ committer.commit
+
+ nil
+ end
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 0d1ee73ca1a..db7c29be94b 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -2,8 +2,6 @@
# class return an instance of `GitlabAccessStatus`
module Gitlab
class GitAccess
- include Gitlab::Utils::StrongMemoize
-
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
@@ -17,7 +15,6 @@ module Gitlab
deploy_key_upload: 'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.',
project_not_found: 'The project you were looking for could not be found.',
- account_blocked: 'Your account has been blocked.',
command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
@@ -108,8 +105,11 @@ module Gitlab
end
def check_active_user!
- if user && !user_access.allowed?
- raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
+ return unless user
+
+ unless user_access.allowed?
+ message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
+ raise UnauthorizedError, message
end
end
@@ -340,6 +340,8 @@ module Gitlab
def user_access
@user_access ||= if ci?
CiAccess.new
+ elsif user && request_from_ci_build?
+ BuildAccess.new(user, project: project)
else
UserAccess.new(user, project: project)
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index a36e6c822f9..1f5f88bf792 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -334,6 +334,22 @@ module Gitlab
signatures
end
+ def get_commit_messages(commit_ids)
+ request = Gitaly::GetCommitMessagesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
+ response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_messages, request)
+
+ messages = Hash.new { |h, k| h[k] = ''.b }
+ current_commit_id = nil
+
+ response.each do |rpc_message|
+ current_commit_id = rpc_message.commit_id if rpc_message.commit_id.present?
+
+ messages[current_commit_id] << rpc_message.message
+ end
+
+ messages
+ end
+
private
def call_commit_diff(request_params, options = {})
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 831cfd1e014..44b0e517bf0 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -40,7 +40,7 @@ module Gitlab
raise Gitlab::Git::Repository::TagExistsError
end
- Util.gitlab_tag_from_gitaly_tag(@repository, response.tag)
+ Gitlab::Git::Tag.new(@repository, response.tag)
rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::Repository::InvalidRef, e
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index ba6b577fd17..3ac46be6208 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -171,6 +171,22 @@ module Gitlab
consume_ref_contains_sha_response(stream, :branch_names)
end
+ def get_tag_messages(tag_ids)
+ request = Gitaly::GetTagMessagesRequest.new(repository: @gitaly_repo, tag_ids: tag_ids)
+ response = GitalyClient.call(@repository.storage, :ref_service, :get_tag_messages, request)
+
+ messages = Hash.new { |h, k| h[k] = ''.b }
+ current_tag_id = nil
+
+ response.each do |rpc_message|
+ current_tag_id = rpc_message.tag_id if rpc_message.tag_id.present?
+
+ messages[current_tag_id] << rpc_message.message
+ end
+
+ messages
+ end
+
private
def consume_refs_response(response)
@@ -210,7 +226,7 @@ module Gitlab
def consume_tags_response(response)
response.flat_map do |message|
- message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) }
+ message.tags.map { |gitaly_tag| Gitlab::Git::Tag.new(@repository, gitaly_tag) }
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 39057beefba..ee01f5a5bd9 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -142,7 +142,7 @@ module Gitlab
:repository_service,
:is_rebase_in_progress,
request,
- timeout: GitalyClient.default_timeout
+ timeout: GitalyClient.fast_timeout
)
response.in_progress
@@ -159,7 +159,7 @@ module Gitlab
:repository_service,
:is_squash_in_progress,
request,
- timeout: GitalyClient.default_timeout
+ timeout: GitalyClient.fast_timeout
)
response.in_progress
@@ -235,6 +235,22 @@ module Gitlab
)
end
+ def create_from_snapshot(http_url, http_auth)
+ request = Gitaly::CreateRepositoryFromSnapshotRequest.new(
+ repository: @gitaly_repo,
+ http_url: http_url,
+ http_auth: http_auth
+ )
+
+ GitalyClient.call(
+ @storage,
+ :repository_service,
+ :create_repository_from_snapshot,
+ request,
+ timeout: GitalyClient.default_timeout
+ )
+ end
+
def write_ref(ref_path, ref, old_ref, shell)
request = Gitaly::WriteRefRequest.new(
repository: @gitaly_repo,
@@ -276,6 +292,24 @@ module Gitlab
request = Gitaly::CalculateChecksumRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :calculate_checksum, request)
response.checksum.presence
+ rescue GRPC::DataLoss => e
+ raise Gitlab::Git::Repository::InvalidRepository.new(e)
+ end
+
+ def raw_changes_between(from, to)
+ request = Gitaly::GetRawChangesRequest.new(repository: @gitaly_repo, from_revision: from, to_revision: to)
+
+ GitalyClient.call(@storage, :repository_service, :get_raw_changes, request)
+ end
+
+ def search_files_by_name(ref, query)
+ request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query)
+ GitalyClient.call(@storage, :repository_service, :search_files_by_name, request).flat_map(&:files)
+ end
+
+ def search_files_by_content(ref, query)
+ request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query)
+ GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches)
end
end
end
diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb
new file mode 100644
index 00000000000..eb0e910665b
--- /dev/null
+++ b/lib/gitlab/gitaly_client/storage_service.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module GitalyClient
+ class StorageService
+ def initialize(storage)
+ @storage = storage
+ end
+
+ # Delete all repositories in the storage. This is a slow and VERY DESTRUCTIVE operation.
+ def delete_all_repositories
+ request = Gitaly::DeleteAllRepositoriesRequest.new(storage_name: @storage)
+ GitalyClient.call(@storage, :storage_service, :delete_all_repositories, request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 8668caf0c55..9a576e463e3 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -5,6 +5,14 @@ module Gitlab
# directly.
class StorageSettings
DirectPathAccessError = Class.new(StandardError)
+ InvalidConfigurationError = Class.new(StandardError)
+
+ INVALID_STORAGE_MESSAGE = <<~MSG.freeze
+ Storage is invalid because it has no `path` key.
+
+ For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.
+ If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.
+ MSG
# This class will give easily recognizable NoMethodErrors
Deprecated = Class.new
@@ -12,7 +20,8 @@ module Gitlab
attr_reader :legacy_disk_path
def initialize(storage)
- raise "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
+ raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
+ raise InvalidConfigurationError, INVALID_STORAGE_MESSAGE unless storage.has_key?('path')
# Support a nil 'path' field because some of the circuit breaker tests use it.
@legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path']
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index 405567db94a..9c19c51d412 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -21,20 +21,6 @@ module Gitlab
gitaly_repository.relative_path,
gitaly_repository.gl_repository)
end
-
- def gitlab_tag_from_gitaly_tag(repository, gitaly_tag)
- if gitaly_tag.target_commit.present?
- commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit)
- end
-
- Gitlab::Git::Tag.new(
- repository,
- Gitlab::EncodingHelper.encode!(gitaly_tag.name.dup),
- gitaly_tag.id,
- commit,
- Gitlab::EncodingHelper.encode!(gitaly_tag.message.chomp)
- )
- end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 7a698e4b3f3..2dfe055a496 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -200,6 +200,8 @@ module Gitlab
def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new(
+ user_id: commit_details.user_id,
+ user_name: encode_binary(commit_details.username),
name: encode_binary(commit_details.name),
email: encode_binary(commit_details.email),
message: encode_binary(commit_details.message)
diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb
index 6da11e6ef08..b02b123c98e 100644
--- a/lib/gitlab/github_import/parallel_importer.rb
+++ b/lib/gitlab/github_import/parallel_importer.rb
@@ -32,7 +32,8 @@ module Gitlab
Gitlab::SidekiqStatus
.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
- project.update_column(:import_jid, jid)
+ project.ensure_import_state
+ project.import_state&.update_column(:jid, jid)
Stage::ImportRepositoryWorker
.perform_async(project.id)
diff --git a/lib/gitlab/gl_id.rb b/lib/gitlab/gl_id.rb
index 624fd00367e..a53d156b41f 100644
--- a/lib/gitlab/gl_id.rb
+++ b/lib/gitlab/gl_id.rb
@@ -2,10 +2,14 @@ module Gitlab
module GlId
def self.gl_id(user)
if user.present?
- "user-#{user.id}"
+ gl_id_from_id_value(user.id)
else
- ""
+ ''
end
end
+
+ def self.gl_id_from_id_value(id)
+ "user-#{id}"
+ end
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index a7e055ac444..0d31934347f 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -15,10 +15,11 @@ module Gitlab
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
gon.gitlab_url = Gitlab.config.gitlab.url
- gon.revision = Gitlab::REVISION
+ gon.revision = Gitlab.revision
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
+ gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites')
gon.test_env = Rails.env.test?
gon.suggested_label_colors = LabelsHelper.suggested_colors
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index af203ff711d..b713fa7e1cd 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.2'.freeze
+ VERSION = '0.2.3'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index cd840bd5b01..21ac7f7e0b6 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -27,8 +27,6 @@ project_tree:
- :releases
- project_members:
- :user
- - lfs_file_locks:
- - :user
- merge_requests:
- notes:
- :author
@@ -66,6 +64,7 @@ project_tree:
- :project_feature
- :custom_attributes
- :project_badges
+ - :ci_cd_settings
# Only include the following attributes for the models specified.
included_attributes:
@@ -75,6 +74,8 @@ included_attributes:
- :username
author:
- :name
+ ci_cd_settings:
+ - :group_runners_enabled
# Do not include the following attributes for the models specified.
excluded_attributes:
@@ -105,6 +106,7 @@ excluded_attributes:
- :last_repository_updated_at
- :last_repository_check_at
- :storage_version
+ - :remote_mirror_available_overridden
- :description_html
snippets:
- :expired_at
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 598832fb2df..4a41a69840b 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -17,7 +17,8 @@ module Gitlab
auto_devops: :project_auto_devops,
label: :project_label,
custom_attributes: 'ProjectCustomAttribute',
- project_badges: 'Badge' }.freeze
+ project_badges: 'Badge',
+ ci_cd_settings: 'ProjectCiCdSetting' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
@@ -27,7 +28,7 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature].freeze
TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index c9122a23568..d323cb9dadf 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -57,7 +57,7 @@ module Gitlab
regex = Regexp.escape(wildcard_address)
regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
- Regexp.new(regex).freeze
+ Regexp.new(/\A#{regex}\z/).freeze
end
end
end
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 6e4df05aa7e..3d778da90c7 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -15,6 +15,9 @@ 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 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/
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index 7edd0ad2033..b04d678cf98 100644
--- a/lib/gitlab/legacy_github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -78,7 +78,8 @@ module Gitlab
def handle_errors
return unless errors.any?
- project.update_column(:import_error, {
+ project.ensure_import_state
+ project.import_state&.update_column(:last_error, {
message: 'The remote data could not be fully imported.',
errors: errors
}.to_json)
diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb
index d12ba0ec176..d41a855bff1 100644
--- a/lib/gitlab/metrics/prometheus.rb
+++ b/lib/gitlab/metrics/prometheus.rb
@@ -25,6 +25,14 @@ module Gitlab
end
end
+ def reset_registry!
+ clear_memoization(:registry)
+
+ REGISTRY_MUTEX.synchronize do
+ ::Prometheus::Client.reset!
+ end
+ end
+
def registry
strong_memoize(:registry) do
REGISTRY_MUTEX.synchronize do
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
index 89ff02a96d6..3799aaebf1c 100644
--- a/lib/gitlab/metrics/web_transaction.rb
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -4,18 +4,6 @@ module Gitlab
CONTROLLER_KEY = 'action_controller.instance'.freeze
ENDPOINT_KEY = 'api.endpoint'.freeze
- CONTENT_TYPES = {
- 'text/html' => :html,
- 'text/plain' => :txt,
- 'application/json' => :json,
- 'text/js' => :js,
- 'application/atom+xml' => :atom,
- 'image/png' => :png,
- 'image/jpeg' => :jpeg,
- 'image/gif' => :gif,
- 'image/svg+xml' => :svg
- }.freeze
-
def initialize(env)
super()
@env = env
@@ -40,7 +28,11 @@ module Gitlab
controller = @env[CONTROLLER_KEY]
action = "#{controller.action_name}"
- suffix = CONTENT_TYPES[controller.content_type]
+
+ # Devise exposes a method called "request_format" that does the below.
+ # However, this method is not available to all controllers (e.g. certain
+ # Doorkeeper controllers). As such we use the underlying code directly.
+ suffix = controller.request.format.try(:ref)
if suffix && suffix != :html
action += ".#{suffix}"
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
deleted file mode 100644
index 6aecf63231f..00000000000
--- a/lib/gitlab/middleware/webpack_proxy.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# This Rack middleware is intended to proxy the webpack assets directory to the
-# webpack-dev-server. It is only intended for use in development.
-
-# :nocov:
-module Gitlab
- module Middleware
- class WebpackProxy < Rack::Proxy
- def initialize(app = nil, opts = {})
- @proxy_host = opts.fetch(:proxy_host, 'localhost')
- @proxy_port = opts.fetch(:proxy_port, 3808)
- @proxy_path = opts[:proxy_path] if opts[:proxy_path]
-
- super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
- end
-
- def perform_request(env)
- if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
- super(env)
- else
- @app.call(env)
- end
- end
- end
- end
-end
-# :nocov:
diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb
index 43921a8c1c0..fd5de73c526 100644
--- a/lib/gitlab/multi_collection_paginator.rb
+++ b/lib/gitlab/multi_collection_paginator.rb
@@ -5,7 +5,7 @@ module Gitlab
def initialize(*collections, per_page: nil)
raise ArgumentError.new('Only 2 collections are supported') if collections.size != 2
- @per_page = per_page || Kaminari.config.default_per_page
+ @per_page = (per_page || Kaminari.config.default_per_page).to_i
@first_collection, @second_collection = collections
end
diff --git a/lib/gitlab/pages_client.rb b/lib/gitlab/pages_client.rb
new file mode 100644
index 00000000000..7b358a3bd1b
--- /dev/null
+++ b/lib/gitlab/pages_client.rb
@@ -0,0 +1,117 @@
+module Gitlab
+ class PagesClient
+ class << self
+ attr_reader :certificate, :token
+
+ def call(service, rpc, request, timeout: nil)
+ kwargs = request_kwargs(timeout)
+ stub(service).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ # This function is not thread-safe. Call it from an initializer only.
+ def read_or_create_token
+ @token = read_token
+ rescue Errno::ENOENT
+ # TODO: uncomment this when omnibus knows how to write the token file for us
+ # https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466
+ #
+ # write_token(SecureRandom.random_bytes(64))
+ #
+ # # Read from disk in case someone else won the race and wrote the file
+ # # before us. If this fails again let the exception bubble up.
+ # @token = read_token
+ end
+
+ # This function is not thread-safe. Call it from an initializer only.
+ def load_certificate
+ cert_path = config.certificate
+ return unless cert_path.present?
+
+ @certificate = File.read(cert_path)
+ end
+
+ def ping
+ request = Grpc::Health::V1::HealthCheckRequest.new
+ call(:health_check, :check, request, timeout: 5.seconds)
+ end
+
+ private
+
+ def request_kwargs(timeout)
+ encoded_token = Base64.strict_encode64(token.to_s)
+ metadata = {
+ 'authorization' => "Bearer #{encoded_token}"
+ }
+
+ result = { metadata: metadata }
+
+ return result unless timeout
+
+ # Do not use `Time.now` for deadline calculation, since it
+ # will be affected by Timecop in some tests, but grpc's c-core
+ # uses system time instead of timecop's time, so tests will fail
+ # `Time.at(Process.clock_gettime(Process::CLOCK_REALTIME))` will
+ # circumvent timecop
+ deadline = Time.at(Process.clock_gettime(Process::CLOCK_REALTIME)) + timeout
+ result[:deadline] = deadline
+
+ result
+ end
+
+ def stub(name)
+ stub_class(name).new(address, grpc_creds)
+ end
+
+ def stub_class(name)
+ if name == :health_check
+ Grpc::Health::V1::Health::Stub
+ else
+ # TODO use pages namespace
+ Gitaly.const_get(name.to_s.camelcase.to_sym).const_get(:Stub)
+ end
+ end
+
+ def address
+ addr = config.address
+ addr = addr.sub(%r{^tcp://}, '') if URI(addr).scheme == 'tcp'
+ addr
+ end
+
+ def grpc_creds
+ if address.start_with?('unix:')
+ :this_channel_is_insecure
+ elsif @certificate
+ GRPC::Core::ChannelCredentials.new(@certificate)
+ else
+ # Use system certificate pool
+ GRPC::Core::ChannelCredentials.new
+ end
+ end
+
+ def config
+ Gitlab.config.pages.admin
+ end
+
+ def read_token
+ File.read(token_path)
+ end
+
+ def token_path
+ Rails.root.join('.gitlab_pages_secret').to_s
+ end
+
+ def write_token(new_token)
+ Tempfile.open(File.basename(token_path), File.dirname(token_path), encoding: 'ascii-8bit') do |f|
+ f.write(new_token)
+ f.close
+ File.link(f.path, token_path)
+ end
+ rescue Errno::EACCES => ex
+ # TODO stop rescuing this exception in GitLab 11.0 https://gitlab.com/gitlab-org/gitlab-ce/issues/45672
+ Rails.logger.error("Could not write pages admin token file: #{ex}")
+ rescue Errno::EEXIST
+ # Another process wrote the token file concurrently with us. Use their token, not ours.
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 390efda326a..2e9b6e302f5 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -59,7 +59,7 @@ module Gitlab
startline = 0
result.each_line.each_with_index do |line, index|
- prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>.*)\x00(?<startline>\d+)\x00/)&.tap do |matches|
+ prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/)&.tap do |matches|
ref = matches[:ref]
filename = matches[:filename]
startline = matches[:startline]
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index ae136202f0c..08f6a54776f 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -25,9 +25,9 @@ module Gitlab
end
TEMPLATES_TABLE = [
- ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
- ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
- ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
+ ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'),
+ ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw and pom.xml to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'),
+ ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express')
].freeze
class << self
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index 10bec7a90da..e5a0fdae7ef 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -5,6 +5,8 @@ module Gitlab
module Redis
class SharedState < ::Gitlab::Redis::Wrapper
SESSION_NAMESPACE = 'session:gitlab'.freeze
+ USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze
+ USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze
DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze
REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 1fa2a19b0af..4888184403c 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -4,7 +4,8 @@ module Gitlab
def self.parse(repo_path)
wiki = false
- project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false)
+ project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '')
+
project, was_redirected = find_project(project_path)
if project_path.end_with?('.wiki') && project.nil?
@@ -17,22 +18,6 @@ module Gitlab
[project, wiki, redirected_path]
end
- def self.strip_storage_path(repo_path, fail_on_not_found: true)
- result = repo_path
-
- storage = Gitlab.config.repositories.storages.values.find do |params|
- repo_path.start_with?(params.legacy_disk_path)
- end
-
- if storage
- result = result.sub(storage.legacy_disk_path, '')
- elsif fail_on_not_found
- raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
- end
-
- result.sub(%r{\A/*}, '')
- end
-
def self.find_project(project_path)
project = Project.find_by_full_path(project_path, follow_redirects: true)
was_redirected = project && project.full_path.casecmp(project_path) != 0
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
index 9c92b83dddc..6bb00d8ae21 100644
--- a/lib/gitlab/serializer/pagination.rb
+++ b/lib/gitlab/serializer/pagination.rb
@@ -17,8 +17,6 @@ module Gitlab
end
end
- private
-
# Methods needed by `API::Helpers::Pagination`
#
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index ac4ac537a8a..4a691d640b3 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -65,11 +65,11 @@ module Gitlab
# Init new repository
#
- # storage - project's storage name
+ # storage - the shard key
# name - project disk path
#
# Ex.
- # create_repository("/path/to/storage", "gitlab/gitlab-ci")
+ # create_repository("default", "gitlab/gitlab-ci")
#
def create_repository(storage, name)
relative_path = name.dup
@@ -291,20 +291,10 @@ module Gitlab
# Add empty directory for storing repositories
#
# Ex.
- # add_namespace("/path/to/storage", "gitlab")
+ # add_namespace("default", "gitlab")
#
def add_namespace(storage, name)
- Gitlab::GitalyClient.migrate(:add_namespace,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- gitaly_namespace_client(storage).add(name)
- else
- path = full_path(storage, name)
- FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
- end
- end
- rescue Errno::EEXIST => e
- Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
+ Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message
end
@@ -313,17 +303,10 @@ module Gitlab
# Every repository inside this directory will be removed too
#
# Ex.
- # rm_namespace("/path/to/storage", "gitlab")
+ # rm_namespace("default", "gitlab")
#
def rm_namespace(storage, name)
- Gitlab::GitalyClient.migrate(:remove_namespace,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- gitaly_namespace_client(storage).remove(name)
- else
- FileUtils.rm_r(full_path(storage, name), force: true)
- end
- end
+ Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
rescue GRPC::InvalidArgument => e
raise ArgumentError, e.message
end
@@ -335,16 +318,7 @@ module Gitlab
# mv_namespace("/path/to/storage", "gitlab", "gitlabhq")
#
def mv_namespace(storage, old_name, new_name)
- Gitlab::GitalyClient.migrate(:rename_namespace,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- gitaly_namespace_client(storage).rename(old_name, new_name)
- else
- break false if exists?(storage, new_name) || !exists?(storage, old_name)
-
- FileUtils.mv(full_path(storage, old_name), full_path(storage, new_name))
- end
- end
+ Gitlab::GitalyClient::NamespaceService.new(storage).rename(old_name, new_name)
rescue GRPC::InvalidArgument
false
end
@@ -369,16 +343,8 @@ module Gitlab
# exists?(storage, 'gitlab')
# exists?(storage, 'gitlab/cookies.git')
#
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385
def exists?(storage, dir_name)
- Gitlab::GitalyClient.migrate(:namespace_exists,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
- if enabled
- gitaly_namespace_client(storage).exists?(dir_name)
- else
- File.exist?(full_path(storage, dir_name))
- end
- end
+ Gitlab::GitalyClient::NamespaceService.new(storage).exists?(dir_name)
end
protected
@@ -398,7 +364,7 @@ module Gitlab
def full_path(storage, dir_name)
raise ArgumentError.new("Directory name can't be blank") if dir_name.blank?
- File.join(storage, dir_name)
+ File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, dir_name)
end
def gitlab_shell_projects_path
@@ -475,14 +441,6 @@ module Gitlab
Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
end
- def gitaly_namespace_client(storage_path)
- storage, _value = Gitlab.config.repositories.storages.find do |storage, value|
- value.legacy_disk_path == storage_path
- end
-
- Gitlab::GitalyClient::NamespaceService.new(storage)
- end
-
def git_timeout
Gitlab.config.gitlab_shell.git_timeout
end
diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb
index 7ce2e9d636e..dc2d91dfa23 100644
--- a/lib/gitlab/untrusted_regexp.rb
+++ b/lib/gitlab/untrusted_regexp.rb
@@ -9,9 +9,15 @@ module Gitlab
# there is a strict limit on total execution time. See the RE2 documentation
# at https://github.com/google/re2/wiki/Syntax for more details.
class UntrustedRegexp
- delegate :===, to: :regexp
+ require_dependency 're2'
+
+ delegate :===, :source, to: :regexp
+
+ def initialize(pattern, multiline: false)
+ if multiline
+ pattern = "(?m)#{pattern}"
+ end
- def initialize(pattern)
@regexp = RE2::Regexp.new(pattern, log_errors: false)
raise RegexpError.new(regexp.error) unless regexp.ok?
@@ -31,6 +37,41 @@ module Gitlab
RE2.Replace(text, regexp, rewrite)
end
+ def ==(other)
+ self.source == other.source
+ end
+
+ # Handles regular expressions with the preferred RE2 library where possible
+ # via UntustedRegex. Falls back to Ruby's built-in regular expression library
+ # when the syntax would be invalid in RE2.
+ #
+ # One difference between these is `(?m)` multi-line mode. Ruby regex enables
+ # this by default, but also handles `^` and `$` differently.
+ # See: https://www.regular-expressions.info/modifiers.html
+ def self.with_fallback(pattern, multiline: false)
+ UntrustedRegexp.new(pattern, multiline: multiline)
+ rescue RegexpError
+ Regexp.new(pattern)
+ end
+
+ def self.valid?(pattern)
+ !!self.fabricate(pattern)
+ rescue RegexpError
+ false
+ end
+
+ def self.fabricate(pattern)
+ matches = pattern.match(%r{^/(?<regexp>.+)/(?<flags>[ismU]*)$})
+
+ raise RegexpError, 'Invalid regular expression!' if matches.nil?
+
+ expression = matches[:regexp]
+ flags = matches[:flags]
+ expression.prepend("(?#{flags})") if flags.present?
+
+ self.new(expression, multiline: false)
+ end
+
private
attr_reader :regexp
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 8c0a4d55ea2..e294f3c4ebc 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -71,6 +71,7 @@ module Gitlab
projects_imported_from_github: Project.where(import_type: 'github').count,
protected_branches: ProtectedBranch.count,
releases: Release.count,
+ remote_mirrors: RemoteMirror.count,
snippets: Snippet.count,
todos: Todo.count,
uploads: Upload.count,
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 69952cbb47c..8cf5d636743 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -63,10 +63,12 @@ module Gitlab
request_cache def can_push_to_branch?(ref)
return false unless can_access_git?
- return false unless user.can?(:push_code, project) || project.branch_allows_maintainer_push?(user, ref)
+ return false unless project
+
+ return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref)
if protected?(ProtectedBranch, project, ref)
- project.user_can_push_to_empty_repo?(user) || protected_branch_accessible_to?(ref, action: :push)
+ protected_branch_accessible_to?(ref, action: :push)
else
true
end
@@ -101,6 +103,7 @@ module Gitlab
def protected_branch_accessible_to?(ref, action:)
ProtectedBranch.protected_ref_accessible_to?(
ref, user,
+ project: project,
action: action,
protected_refs: project.protected_branches)
end
@@ -108,6 +111,7 @@ module Gitlab
def protected_tag_accessible_to?(ref, action:)
ProtectedTag.protected_ref_accessible_to?(
ref, user,
+ project: project,
action: action,
protected_refs: project.protected_tags)
end
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
index 841fb681435..36162faa1eb 100644
--- a/lib/gitlab/view/presenter/base.rb
+++ b/lib/gitlab/view/presenter/base.rb
@@ -20,6 +20,10 @@ module Gitlab
subject
end
+ def present(**attributes)
+ self
+ end
+
class_methods do
def presenter?
true
diff --git a/lib/gitlab/webpack/dev_server_middleware.rb b/lib/gitlab/webpack/dev_server_middleware.rb
new file mode 100644
index 00000000000..b9a75eaac63
--- /dev/null
+++ b/lib/gitlab/webpack/dev_server_middleware.rb
@@ -0,0 +1,26 @@
+# This Rack middleware is intended to proxy the webpack assets directory to the
+# webpack-dev-server. It is only intended for use in development.
+
+# :nocov:
+module Gitlab
+ module Webpack
+ class DevServerMiddleware < Rack::Proxy
+ def initialize(app = nil, opts = {})
+ @proxy_host = opts.fetch(:proxy_host, 'localhost')
+ @proxy_port = opts.fetch(:proxy_port, 3808)
+ @proxy_path = opts[:proxy_path] if opts[:proxy_path]
+
+ super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts)
+ end
+
+ def perform_request(env)
+ if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ super(env)
+ else
+ @app.call(env)
+ end
+ end
+ end
+ end
+end
+# :nocov:
diff --git a/lib/gitlab/webpack/manifest.rb b/lib/gitlab/webpack/manifest.rb
new file mode 100644
index 00000000000..0c343e5bc1d
--- /dev/null
+++ b/lib/gitlab/webpack/manifest.rb
@@ -0,0 +1,27 @@
+require 'webpack/rails/manifest'
+
+module Gitlab
+ module Webpack
+ class Manifest < ::Webpack::Rails::Manifest
+ # Raised if a supplied asset does not exist in the webpack manifest
+ AssetMissingError = Class.new(StandardError)
+
+ class << self
+ def entrypoint_paths(source)
+ raise ::Webpack::Rails::Manifest::WebpackError, manifest["errors"] unless manifest_bundled?
+
+ entrypoint = manifest["entrypoints"][source]
+ if entrypoint && entrypoint["assets"]
+ # Can be either a string or an array of strings.
+ # Do not include source maps as they are not javascript
+ [entrypoint["assets"]].flatten.reject { |p| p =~ /.*\.map$/ }.map do |p|
+ "/#{::Rails.configuration.webpack.public_path}/#{p}"
+ end
+ else
+ raise AssetMissingError, "Can't find entry point '#{source}' in webpack manifest"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 153cb2a8bb1..e893e46ee86 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -65,12 +65,7 @@ module Gitlab
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha)
raise "Repository or ref not found" if params.empty?
- if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
- params.merge!(
- 'GitalyServer' => gitaly_server_hash(repository),
- 'GitalyRepository' => repository.gitaly_repository.to_h
- )
- end
+ params['GitalyServer'] = gitaly_server_hash(repository)
# If present DisableCache must be a Boolean. Otherwise workhorse ignores it.
params['DisableCache'] = true if git_archive_cache_disabled?
@@ -81,6 +76,20 @@ module Gitlab
]
end
+ def send_git_snapshot(repository)
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
+ repository: repository.gitaly_repository
+ ).to_json
+ }
+
+ [
+ SEND_DATA_HEADER,
+ "git-snapshot:#{encode(params)}"
+ ]
+ end
+
def send_git_diff(repository, diff_refs)
params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
diff --git a/lib/omni_auth/strategies/jwt.rb b/lib/omni_auth/strategies/jwt.rb
new file mode 100644
index 00000000000..2349b2a28aa
--- /dev/null
+++ b/lib/omni_auth/strategies/jwt.rb
@@ -0,0 +1,62 @@
+require 'omniauth'
+require 'jwt'
+
+module OmniAuth
+ module Strategies
+ class JWT
+ ClaimInvalid = Class.new(StandardError)
+
+ include OmniAuth::Strategy
+
+ args [:secret]
+
+ option :secret, nil
+ option :algorithm, 'HS256'
+ option :uid_claim, 'email'
+ option :required_claims, %w(name email)
+ option :info_map, { name: "name", email: "email" }
+ option :auth_url, nil
+ option :valid_within, nil
+
+ uid { decoded[options.uid_claim] }
+
+ extra do
+ { raw_info: decoded }
+ end
+
+ info do
+ options.info_map.each_with_object({}) do |(k, v), h|
+ h[k.to_s] = decoded[v.to_s]
+ end
+ end
+
+ def request_phase
+ redirect options.auth_url
+ end
+
+ def decoded
+ @decoded ||= ::JWT.decode(request.params['jwt'], options.secret, options.algorithm).first
+
+ (options.required_claims || []).each do |field|
+ raise ClaimInvalid, "Missing required '#{field}' claim" unless @decoded.key?(field.to_s)
+ end
+
+ raise ClaimInvalid, "Missing required 'iat' claim" if options.valid_within && !@decoded["iat"]
+
+ if options.valid_within && (Time.now.to_i - @decoded["iat"]).abs > options.valid_within
+ raise ClaimInvalid, "'iat' timestamp claim is too skewed from present"
+ end
+
+ @decoded
+ end
+
+ def callback_phase
+ super
+ rescue ClaimInvalid => e
+ fail! :claim_invalid, e
+ end
+ end
+
+ class Jwt < JWT; end
+ end
+end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 24e37f6c6cc..e96fbb64372 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -6,7 +6,6 @@ namespace :gitlab do
desc "GitLab | Create a backup of the GitLab system"
task create: :gitlab_environment do
warn_user_is_not_gitlab
- configure_cron_mode
Rake::Task["gitlab:backup:db:create"].invoke
Rake::Task["gitlab:backup:repo:create"].invoke
@@ -17,7 +16,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
- backup = Backup::Manager.new
+ backup = Backup::Manager.new(progress)
backup.pack
backup.cleanup
backup.remove_old
@@ -27,9 +26,8 @@ namespace :gitlab do
desc 'GitLab | Restore a previously created backup'
task restore: :gitlab_environment do
warn_user_is_not_gitlab
- configure_cron_mode
- backup = Backup::Manager.new
+ backup = Backup::Manager.new(progress)
backup.unpack
unless backup.skipped?('db')
@@ -49,9 +47,9 @@ namespace :gitlab do
# Drop all tables Load the schema to ensure we don't have any newer tables
# hanging out from a failed upgrade
- $progress.puts 'Cleaning the database ... '.color(:blue)
+ progress.puts 'Cleaning the database ... '.color(:blue)
Rake::Task['gitlab:db:drop_tables'].invoke
- $progress.puts 'done'.color(:green)
+ progress.puts 'done'.color(:green)
Rake::Task['gitlab:backup:db:restore'].invoke
rescue Gitlab::TaskAbortedByUserError
puts "Quitting...".color(:red)
@@ -74,173 +72,173 @@ namespace :gitlab do
namespace :repo do
task create: :gitlab_environment do
- $progress.puts "Dumping repositories ...".color(:blue)
+ progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Repository.new.dump
- $progress.puts "done".color(:green)
+ Backup::Repository.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring repositories ...".color(:blue)
- Backup::Repository.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring repositories ...".color(:blue)
+ Backup::Repository.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :db do
task create: :gitlab_environment do
- $progress.puts "Dumping database ... ".color(:blue)
+ progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Database.new.dump
- $progress.puts "done".color(:green)
+ Backup::Database.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring database ... ".color(:blue)
- Backup::Database.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring database ... ".color(:blue)
+ Backup::Database.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :builds do
task create: :gitlab_environment do
- $progress.puts "Dumping builds ... ".color(:blue)
+ progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Builds.new.dump
- $progress.puts "done".color(:green)
+ Backup::Builds.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring builds ... ".color(:blue)
- Backup::Builds.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring builds ... ".color(:blue)
+ Backup::Builds.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :uploads do
task create: :gitlab_environment do
- $progress.puts "Dumping uploads ... ".color(:blue)
+ progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Uploads.new.dump
- $progress.puts "done".color(:green)
+ Backup::Uploads.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring uploads ... ".color(:blue)
- Backup::Uploads.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring uploads ... ".color(:blue)
+ Backup::Uploads.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :artifacts do
task create: :gitlab_environment do
- $progress.puts "Dumping artifacts ... ".color(:blue)
+ progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Artifacts.new.dump
- $progress.puts "done".color(:green)
+ Backup::Artifacts.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring artifacts ... ".color(:blue)
- Backup::Artifacts.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring artifacts ... ".color(:blue)
+ Backup::Artifacts.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :pages do
task create: :gitlab_environment do
- $progress.puts "Dumping pages ... ".color(:blue)
+ progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Pages.new.dump
- $progress.puts "done".color(:green)
+ Backup::Pages.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring pages ... ".color(:blue)
- Backup::Pages.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring pages ... ".color(:blue)
+ Backup::Pages.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :lfs do
task create: :gitlab_environment do
- $progress.puts "Dumping lfs objects ... ".color(:blue)
+ progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Lfs.new.dump
- $progress.puts "done".color(:green)
+ Backup::Lfs.new(progress).dump
+ progress.puts "done".color(:green)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring lfs objects ... ".color(:blue)
- Backup::Lfs.new.restore
- $progress.puts "done".color(:green)
+ progress.puts "Restoring lfs objects ... ".color(:blue)
+ Backup::Lfs.new(progress).restore
+ progress.puts "done".color(:green)
end
end
namespace :registry do
task create: :gitlab_environment do
- $progress.puts "Dumping container registry images ... ".color(:blue)
+ progress.puts "Dumping container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
if ENV["SKIP"] && ENV["SKIP"].include?("registry")
- $progress.puts "[SKIPPED]".color(:cyan)
+ progress.puts "[SKIPPED]".color(:cyan)
else
- Backup::Registry.new.dump
- $progress.puts "done".color(:green)
+ Backup::Registry.new(progress).dump
+ progress.puts "done".color(:green)
end
else
- $progress.puts "[DISABLED]".color(:cyan)
+ progress.puts "[DISABLED]".color(:cyan)
end
end
task restore: :gitlab_environment do
- $progress.puts "Restoring container registry images ... ".color(:blue)
+ progress.puts "Restoring container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
- Backup::Registry.new.restore
- $progress.puts "done".color(:green)
+ Backup::Registry.new(progress).restore
+ progress.puts "done".color(:green)
else
- $progress.puts "[DISABLED]".color(:cyan)
+ progress.puts "[DISABLED]".color(:cyan)
end
end
end
- def configure_cron_mode
+ def progress
if ENV['CRON']
# We need an object we can say 'puts' and 'print' to; let's use a
# StringIO.
require 'stringio'
- $progress = StringIO.new
+ StringIO.new
else
- $progress = $stdout
+ $stdout
end
end
end # namespace end: backup
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index abef8cd2bcc..c04dae7446f 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -427,10 +427,7 @@ namespace :gitlab do
user = User.find_by(username: username)
if user
repo_dirs = user.authorized_projects.map do |p|
- File.join(
- p.repository_storage_path,
- "#{p.disk_path}.git"
- )
+ p.repository.path_to_repo
end
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index 47ed522aec3..289aa5d9060 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -47,7 +47,7 @@ namespace :gitlab do
puts ""
puts "GitLab information".color(:yellow)
puts "Version:\t#{Gitlab::VERSION}"
- puts "Revision:\t#{Gitlab::REVISION}"
+ puts "Revision:\t#{Gitlab.revision}"
puts "Directory:\t#{Rails.root}"
puts "DB Adapter:\t#{database_adapter}"
puts "URL:\t\t#{Gitlab.config.gitlab.url}"
diff --git a/lib/tasks/gitlab/list_repos.rake b/lib/tasks/gitlab/list_repos.rake
index d7f28691098..b854c34a8e5 100644
--- a/lib/tasks/gitlab/list_repos.rake
+++ b/lib/tasks/gitlab/list_repos.rake
@@ -10,9 +10,8 @@ namespace :gitlab do
end
scope.find_each do |project|
- base = File.join(project.repository_storage_path, project.disk_path)
- puts base + '.git'
- puts base + '.wiki.git'
+ puts project.repository.path_to_repo
+ puts project.wiki.repository.path_to_repo
end
end
end
diff --git a/lib/tasks/gitlab/pages.rake b/lib/tasks/gitlab/pages.rake
new file mode 100644
index 00000000000..100e480bd66
--- /dev/null
+++ b/lib/tasks/gitlab/pages.rake
@@ -0,0 +1,9 @@
+namespace :gitlab do
+ namespace :pages do
+ desc 'Ping the pages admin API'
+ task admin_ping: :gitlab_environment do
+ Gitlab::PagesClient.ping
+ puts "OK: gitlab-pages admin API is reachable"
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 1d903c81358..f71e69987cb 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -1,9 +1,20 @@
namespace :gitlab do
desc "GitLab | Setup production application"
task setup: :gitlab_environment do
+ check_gitaly_connection
setup_db
end
+ def check_gitaly_connection
+ Gitlab.config.repositories.storages.each do |name, _details|
+ Gitlab::GitalyClient::ServerService.new(name).info
+ end
+ rescue GRPC::Unavailable => ex
+ puts "Failed to connect to Gitaly...".color(:red)
+ puts "Error: #{ex}"
+ exit 1
+ end
+
def setup_db
warn_user_is_not_gitlab
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 523b0fa055b..2222807fe13 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -4,7 +4,6 @@ namespace :gitlab do
cmds = [
%w(rake brakeman),
%w(rake rubocop),
- %w(rake spinach),
%w(rake spec),
%w(rake karma)
]
diff --git a/lib/tasks/migrate/add_limits_mysql.rake b/lib/tasks/migrate/add_limits_mysql.rake
index 151f42a2222..9b05876034c 100644
--- a/lib/tasks/migrate/add_limits_mysql.rake
+++ b/lib/tasks/migrate/add_limits_mysql.rake
@@ -1,6 +1,8 @@
require Rails.root.join('db/migrate/limits_to_mysql')
require Rails.root.join('db/migrate/markdown_cache_limits_to_mysql')
require Rails.root.join('db/migrate/merge_request_diff_file_limits_to_mysql')
+require Rails.root.join('db/migrate/limits_ci_build_trace_chunks_raw_data_for_mysql')
+require Rails.root.join('db/migrate/gpg_keys_limits_to_mysql')
desc "GitLab | Add limits to strings in mysql database"
task add_limits_mysql: :environment do
@@ -8,4 +10,6 @@ task add_limits_mysql: :environment do
LimitsToMysql.new.up
MarkdownCacheLimitsToMysql.new.up
MergeRequestDiffFileLimitsToMysql.new.up
+ LimitsCiBuildTraceChunksRawDataForMysql.new.up
+ IncreaseMysqlTextLimitForGpgKeys.new.up
end
diff --git a/lib/tasks/migrate/composite_primary_keys.rake b/lib/tasks/migrate/composite_primary_keys.rake
new file mode 100644
index 00000000000..eb112434dd9
--- /dev/null
+++ b/lib/tasks/migrate/composite_primary_keys.rake
@@ -0,0 +1,15 @@
+namespace :gitlab do
+ namespace :db do
+ desc 'GitLab | Adds primary keys to tables that only have composite unique keys'
+ task composite_primary_keys_add: :environment do
+ require Rails.root.join('db/optional_migrations/composite_primary_keys')
+ CompositePrimaryKeysMigration.new.up
+ end
+
+ desc 'GitLab | Removes previously added composite primary keys'
+ task composite_primary_keys_drop: :environment do
+ require Rails.root.join('db/optional_migrations/composite_primary_keys')
+ CompositePrimaryKeysMigration.new.down
+ end
+ end
+end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index af30ecb0e9b..e7aab50e42a 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -8,6 +8,7 @@ task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb')
+ require Rails.root.join('db/migrate/20180504195842_project_name_lower_index.rb')
require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb')
NamespacesProjectsPathLowerIndexes.new.up
@@ -18,5 +19,6 @@ task setup_postgresql: :environment do
IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up
UsersNameLowerIndex.new.up
+ ProjectNameLowerIndex.new.up
AddPathIndexToRedirectRoutes.new.up
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
deleted file mode 100644
index 19ff13f06c0..00000000000
--- a/lib/tasks/spinach.rake
+++ /dev/null
@@ -1,60 +0,0 @@
-Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach')
-
-namespace :spinach do
- namespace :project do
- desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features"
- task :half do
- run_spinach_tests('@project_commits,@project_issues,@project_merge_requests')
- end
-
- desc "GitLab | Spinach | Run remaining project spinach features"
- task :rest do
- run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests')
- end
- end
-
- desc "GitLab | Spinach | Run project spinach features"
- task :project do
- run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets')
- end
-
- desc "GitLab | Spinach | Run other spinach features"
- task :other do
- run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets')
- end
-
- desc "GitLab | Spinach | Run other spinach features"
- task :builds do
- run_spinach_tests('@builds')
- end
-end
-
-desc "GitLab | Run spinach"
-task :spinach do
- run_spinach_tests(nil)
-end
-
-def run_system_command(cmd)
- system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd)
-end
-
-def run_spinach_command(args)
- run_system_command(%w(spinach -r rerun) + args)
-end
-
-def run_spinach_tests(tags)
- success = run_spinach_command(%W(--tags #{tags}))
- 3.times do |_|
- break if success
- break unless File.exist?('tmp/spinach-rerun.txt')
-
- tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
- puts ''
- puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red)
- puts ''
- sleep(3)
- success = run_spinach_command(tests)
- end
-
- raise("spinach tests for #{tags} failed!") unless success
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0eec9793391..608d2a584ba 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-04-17 11:44+0200\n"
-"PO-Revision-Date: 2018-04-17 11:44+0200\n"
+"POT-Creation-Date: 2018-05-21 12:38-0700\n"
+"PO-Revision-Date: 2018-05-21 12:38-0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -53,6 +53,16 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d staged change"
+msgid_plural "%d staged changes"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%d unstaged change"
+msgid_plural "%d unstaged changes"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] ""
@@ -72,6 +82,12 @@ msgstr[1] ""
msgid "%{loadingIcon} Started"
msgstr ""
+msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
+msgstr ""
+
+msgid "%{nip_domain} can be used as an alternative to a custom domain."
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -84,6 +100,9 @@ msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
+msgid "%{percent}%% complete"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -92,15 +111,62 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{title} changes"
+msgstr ""
+
+msgid "%{unstaged} unstaged and %{staged} staged changes"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
+msgid "- Runner is active and can process any new jobs"
+msgstr ""
+
+msgid "- Runner is paused and will not receive any new jobs"
+msgstr ""
+
msgid "- show less"
msgstr ""
+msgid "1 %{type} addition"
+msgid_plural "%d %{type} additions"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 %{type} modification"
+msgid_plural "%d %{type} modifications"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed issue"
+msgid_plural "%d closed issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 closed merge request"
+msgid_plural "%d closed merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 merged merge request"
+msgid_plural "%d merged merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open issue"
+msgid_plural "%d open issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "1 open merge request"
+msgid_plural "%d open merge requests"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -115,6 +181,9 @@ msgstr ""
msgid "<strong>Removes</strong> source branch"
msgstr ""
+msgid "A 'Runner' is a process which runs a job. You can setup as many Runners as you need."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
@@ -136,6 +205,9 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept terms"
+msgstr ""
+
msgid "Access Tokens"
msgstr ""
@@ -151,6 +223,9 @@ msgstr ""
msgid "Active"
msgstr ""
+msgid "Active Sessions"
+msgstr ""
+
msgid "Activity"
msgstr ""
@@ -259,6 +334,9 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
+msgid "An error occurred while dismissing the alert. Refresh the page and try again."
+msgstr ""
+
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
@@ -343,6 +421,9 @@ msgstr ""
msgid "Artifacts"
msgstr ""
+msgid "Ask your group master to setup a group Runner."
+msgstr ""
+
msgid "Assign custom color like #FF0000"
msgstr ""
@@ -367,6 +448,9 @@ msgstr ""
msgid "Assignee"
msgstr ""
+msgid "Assignee(s)"
+msgstr ""
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
@@ -427,6 +511,12 @@ msgstr ""
msgid "Available"
msgstr ""
+msgid "Available group Runners : %{runners}"
+msgstr ""
+
+msgid "Available group Runners : %{runners}."
+msgstr ""
+
msgid "Avatar will be removed. Are you sure?"
msgstr ""
@@ -669,9 +759,45 @@ msgstr ""
msgid "CI/CD configuration"
msgstr ""
+msgid "CI/CD settings"
+msgstr ""
+
+msgid "CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages."
+msgstr ""
+
+msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery."
+msgstr ""
+
+msgid "CICD|Auto DevOps (Beta)"
+msgstr ""
+
+msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration."
+msgstr ""
+
+msgid "CICD|Disable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Enable Auto DevOps"
+msgstr ""
+
+msgid "CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}."
+msgstr ""
+
+msgid "CICD|Instance default (%{state})"
+msgstr ""
+
msgid "CICD|Jobs"
msgstr ""
+msgid "CICD|Learn more about Auto DevOps"
+msgstr ""
+
+msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project."
+msgstr ""
+
+msgid "Can run untagged jobs"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -825,6 +951,9 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
+msgid "Clear search input"
+msgstr ""
+
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
@@ -915,6 +1044,9 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -1038,6 +1170,9 @@ msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
+msgid "ClusterIntegration|Redeem up to $500 in free credit for Google Cloud Platform"
+msgstr ""
+
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr ""
@@ -1128,6 +1263,15 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "ClusterIntegration|sign up"
+msgstr ""
+
+msgid "Collapse"
+msgstr ""
+
+msgid "Collapse sidebar"
+msgstr ""
+
msgid "Comment and resolve discussion"
msgstr ""
@@ -1246,6 +1390,9 @@ msgstr ""
msgid "Configure limits for web and API requests."
msgstr ""
+msgid "Configure push mirrors."
+msgstr ""
+
msgid "Configure storage path and circuit breaker settings."
msgstr ""
@@ -1303,7 +1450,7 @@ msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
msgstr ""
-msgid "ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images."
+msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images."
msgstr ""
msgid "Continuous Integration and Deployment"
@@ -1411,10 +1558,7 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token"
msgstr ""
-msgid "Creates a new branch from %{branchName}"
-msgstr ""
-
-msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgid "Created"
msgstr ""
msgid "Cron Timezone"
@@ -1423,6 +1567,12 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
+msgid "CurrentUser|Profile"
+msgstr ""
+
+msgid "CurrentUser|Settings"
+msgstr ""
+
msgid "Custom notification events"
msgstr ""
@@ -1465,6 +1615,9 @@ msgstr ""
msgid "December"
msgstr ""
+msgid "Decline and sign out"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr ""
@@ -1479,6 +1632,54 @@ msgstr[1] ""
msgid "Deploy Keys"
msgstr ""
+msgid "DeployKeys|+%{count} others"
+msgstr ""
+
+msgid "DeployKeys|Current project"
+msgstr ""
+
+msgid "DeployKeys|Deploy key"
+msgstr ""
+
+msgid "DeployKeys|Enabled deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error enabling deploy key"
+msgstr ""
+
+msgid "DeployKeys|Error getting deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Error removing deploy key"
+msgstr ""
+
+msgid "DeployKeys|Expand %{count} other projects"
+msgstr ""
+
+msgid "DeployKeys|Loading deploy keys"
+msgstr ""
+
+msgid "DeployKeys|No deploy keys found. Create one with the form above."
+msgstr ""
+
+msgid "DeployKeys|Privately accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Project usage"
+msgstr ""
+
+msgid "DeployKeys|Publicly accessible deploy keys"
+msgstr ""
+
+msgid "DeployKeys|Read access only"
+msgstr ""
+
+msgid "DeployKeys|Write access allowed"
+msgstr ""
+
+msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
+msgstr ""
+
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
@@ -1563,12 +1764,27 @@ msgstr ""
msgid "Directory name"
msgstr ""
+msgid "Disable"
+msgstr ""
+
+msgid "Disable for this project"
+msgstr ""
+
+msgid "Disable group Runners"
+msgstr ""
+
+msgid "Discard changes"
+msgstr ""
+
msgid "Discard draft"
msgstr ""
msgid "Dismiss Cycle Analytics introduction box"
msgstr ""
+msgid "Domain"
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -1608,6 +1824,9 @@ msgstr ""
msgid "Due date"
msgstr ""
+msgid "Each Runner can be in one of the following states:"
+msgstr ""
+
msgid "Edit"
msgstr ""
@@ -1617,9 +1836,6 @@ msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
-msgid "Editing"
-msgstr ""
-
msgid "Email"
msgstr ""
@@ -1629,6 +1845,9 @@ msgstr ""
msgid "Embed"
msgstr ""
+msgid "Enable"
+msgstr ""
+
msgid "Enable Auto DevOps"
msgstr ""
@@ -1641,6 +1860,12 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
+msgid "Enable for this project"
+msgstr ""
+
+msgid "Enable group Runners"
+msgstr ""
+
msgid "Enable or disable version check and usage ping."
msgstr ""
@@ -1650,6 +1875,12 @@ msgstr ""
msgid "Enable the Performance Bar for a given group."
msgstr ""
+msgid "Ends at (UTC)"
+msgstr ""
+
+msgid "Environments"
+msgstr ""
+
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
@@ -1734,6 +1965,9 @@ msgstr ""
msgid "Error updating todo status."
msgstr ""
+msgid "Estimated"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -1761,6 +1995,12 @@ msgstr ""
msgid "Every week (Sundays at 4:00am)"
msgstr ""
+msgid "Expand"
+msgstr ""
+
+msgid "Expand sidebar"
+msgstr ""
+
msgid "Explore projects"
msgstr ""
@@ -1776,6 +2016,9 @@ msgstr ""
msgid "Failed to change the owner"
msgstr ""
+msgid "Failed to check related branches."
+msgstr ""
+
msgid "Failed to remove issue from board, please try again."
msgstr ""
@@ -1794,9 +2037,6 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
-msgid "File name"
-msgstr ""
-
msgid "Files"
msgstr ""
@@ -1853,6 +2093,9 @@ msgstr ""
msgid "GPG Keys"
msgstr ""
+msgid "General"
+msgstr ""
+
msgid "Generate a default set of labels"
msgstr ""
@@ -1874,6 +2117,9 @@ msgstr ""
msgid "GitLab CI Linter has been moved"
msgstr ""
+msgid "GitLab Group Runners can execute code for all the projects in this group."
+msgstr ""
+
msgid "GitLab Runner section"
msgstr ""
@@ -1898,6 +2144,21 @@ msgstr ""
msgid "Got it!"
msgstr ""
+msgid "Graph"
+msgstr ""
+
+msgid "Group CI/CD settings"
+msgstr ""
+
+msgid "Group ID"
+msgstr ""
+
+msgid "Group Runners"
+msgstr ""
+
+msgid "Group masters can register group runners in the %{link}"
+msgstr ""
+
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
@@ -1999,6 +2260,18 @@ msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
+msgid "IDE|Commit"
+msgstr ""
+
+msgid "IDE|Edit"
+msgstr ""
+
+msgid "IDE|Go back"
+msgstr ""
+
+msgid "IDE|Review"
+msgstr ""
+
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr ""
@@ -2020,6 +2293,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Include a Terms of Service agreement that all users must accept."
+msgstr ""
+
msgid "Install Runner on Kubernetes"
msgstr ""
@@ -2029,6 +2305,9 @@ msgstr ""
msgid "Instance does not support multiple Kubernetes clusters"
msgstr ""
+msgid "Integrations"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2166,6 +2445,9 @@ msgstr ""
msgid "LastPushEvent|at"
msgstr ""
+msgid "Latest changes"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -2190,21 +2472,39 @@ msgstr ""
msgid "Leave project"
msgstr ""
+msgid "List"
+msgstr ""
+
msgid "List your GitHub repositories"
msgstr ""
msgid "Loading the GitLab IDE..."
msgstr ""
+msgid "Loading..."
+msgstr ""
+
msgid "Lock"
msgstr ""
msgid "Lock %{issuableDisplayName}"
msgstr ""
+msgid "Lock not found"
+msgstr ""
+
+msgid "Lock to current projects"
+msgstr ""
+
msgid "Locked"
msgstr ""
+msgid "Locked to current projects"
+msgstr ""
+
+msgid "Locked to this project"
+msgstr ""
+
msgid "Login"
msgstr ""
@@ -2226,12 +2526,18 @@ msgstr ""
msgid "March"
msgstr ""
-msgid "Mark done"
+msgid "Mark todo as done"
+msgstr ""
+
+msgid "Markdown enabled"
msgstr ""
msgid "Maximum git storage failures"
msgstr ""
+msgid "Maximum job timeout"
+msgstr ""
+
msgid "May"
msgstr ""
@@ -2241,6 +2547,9 @@ msgstr ""
msgid "Members"
msgstr ""
+msgid "Merge Request:"
+msgstr ""
+
msgid "Merge Requests"
msgstr ""
@@ -2250,6 +2559,9 @@ msgstr ""
msgid "Merge request"
msgstr ""
+msgid "Merge requests"
+msgstr ""
+
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
@@ -2268,6 +2580,9 @@ msgstr ""
msgid "Milestone"
msgstr ""
+msgid "Milestones"
+msgstr ""
+
msgid "Milestones|Delete milestone"
msgstr ""
@@ -2310,6 +2625,9 @@ msgstr ""
msgid "Move issue"
msgstr ""
+msgid "Name"
+msgstr ""
+
msgid "Name new label"
msgstr ""
@@ -2366,6 +2684,9 @@ msgstr ""
msgid "New tag"
msgstr ""
+msgid "No"
+msgstr ""
+
msgid "No assignee"
msgstr ""
@@ -2384,6 +2705,9 @@ msgstr ""
msgid "No file chosen"
msgstr ""
+msgid "No files found."
+msgstr ""
+
msgid "No labels created yet."
msgstr ""
@@ -2516,6 +2840,9 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operations"
+msgstr ""
+
msgid "Options"
msgstr ""
@@ -2552,6 +2879,9 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Pause"
+msgstr ""
+
msgid "Pending"
msgstr ""
@@ -2672,10 +3002,22 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
-msgid "Pipeline|Retry pipeline"
+msgid "Pipeline|Create for"
+msgstr ""
+
+msgid "Pipeline|Create pipeline"
+msgstr ""
+
+msgid "Pipeline|Existing branch name or tag"
+msgstr ""
+
+msgid "Pipeline|Run Pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{pipelineId}?"
+msgid "Pipeline|Search branches"
+msgstr ""
+
+msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
msgid "Pipeline|Stop pipeline"
@@ -2684,7 +3026,7 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
+msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
@@ -2711,6 +3053,9 @@ msgstr ""
msgid "Please <a href=%{link_to_billing} target=\"_blank\" rel=\"noopener noreferrer\">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again."
msgstr ""
+msgid "Please accept the Terms of Service before continuing."
+msgstr ""
+
msgid "Please select at least one filter to see results"
msgstr ""
@@ -2795,6 +3140,12 @@ msgstr ""
msgid "Programming languages used in this repository"
msgstr ""
+msgid "Progress"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
@@ -2846,9 +3197,6 @@ msgstr ""
msgid "ProjectLifecycle|Stage"
msgstr ""
-msgid "ProjectNetworkGraph|Graph"
-msgstr ""
-
msgid "Projects"
msgstr ""
@@ -2996,6 +3344,12 @@ msgstr ""
msgid "Register / Sign In"
msgstr ""
+msgid "Register and see your runners for this group."
+msgstr ""
+
+msgid "Registry"
+msgstr ""
+
msgid "Related Commits"
msgstr ""
@@ -3020,6 +3374,12 @@ msgstr ""
msgid "Remind later"
msgstr ""
+msgid "Remove"
+msgstr ""
+
+msgid "Remove Runner"
+msgstr ""
+
msgid "Remove avatar"
msgstr ""
@@ -3032,12 +3392,18 @@ msgstr ""
msgid "Repository maintenance"
msgstr ""
+msgid "Repository mirror settings"
+msgstr ""
+
msgid "Repository storage"
msgstr ""
msgid "Request Access"
msgstr ""
+msgid "Require all users to accept Terms of Service when they access GitLab."
+msgstr ""
+
msgid "Reset git storage health information"
msgstr ""
@@ -3047,9 +3413,18 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve conflicts on source branch"
+msgstr ""
+
msgid "Resolve discussion"
msgstr ""
+msgid "Resume"
+msgstr ""
+
+msgid "Retry"
+msgstr ""
+
msgid "Retry this job"
msgstr ""
@@ -3079,6 +3454,15 @@ msgstr ""
msgid "Runners"
msgstr ""
+msgid "Runners API"
+msgstr ""
+
+msgid "Runners can be placed on separate users, servers, and even on your local machine."
+msgstr ""
+
+msgid "Runners settings"
+msgstr ""
+
msgid "Running"
msgstr ""
@@ -3109,9 +3493,15 @@ msgstr ""
msgid "Search"
msgstr ""
+msgid "Search branches"
+msgstr ""
+
msgid "Search branches and tags"
msgstr ""
+msgid "Search files"
+msgstr ""
+
msgid "Search milestones"
msgstr ""
@@ -3133,6 +3523,9 @@ msgstr ""
msgid "Select Archive Format"
msgstr ""
+msgid "Select a namespace to fork the project"
+msgstr ""
+
msgid "Select a timezone"
msgstr ""
@@ -3145,6 +3538,9 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
+msgid "Select source branch"
+msgstr ""
+
msgid "Select target branch"
msgstr ""
@@ -3199,6 +3595,9 @@ msgstr ""
msgid "Share"
msgstr ""
+msgid "Shared Runners"
+msgstr ""
+
msgid "Show command"
msgstr ""
@@ -3213,6 +3612,9 @@ msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
+msgid "Sign out"
+msgstr ""
+
msgid "Sign-in restrictions"
msgstr ""
@@ -3351,9 +3753,24 @@ msgstr ""
msgid "Spam and Anti-bot Protection"
msgstr ""
+msgid "Specific Runners"
+msgstr ""
+
msgid "Specify the following URL during the Runner setup:"
msgstr ""
+msgid "Stage all"
+msgstr ""
+
+msgid "Stage changes"
+msgstr ""
+
+msgid "Staged"
+msgstr ""
+
+msgid "Staged %{type}"
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
@@ -3375,6 +3792,9 @@ msgstr ""
msgid "Started"
msgstr ""
+msgid "Starts at (UTC)"
+msgstr ""
+
msgid "Status"
msgstr ""
@@ -3404,6 +3824,9 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "Tags:"
+msgstr ""
+
msgid "TagsPage|Browse commits"
msgstr ""
@@ -3482,6 +3905,12 @@ msgstr ""
msgid "Team"
msgstr ""
+msgid "Terms of Service"
+msgstr ""
+
+msgid "Terms of Service Agreement"
+msgstr ""
+
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
@@ -3587,9 +4016,18 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
+msgid "They can be managed using the %{link}."
+msgstr ""
+
+msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
+msgstr ""
+
msgid "This directory"
msgstr ""
+msgid "This group does not provide any group Runners yet."
+msgstr ""
+
msgid "This is a confidential issue."
msgstr ""
@@ -3638,6 +4076,9 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled while you still have unstaged changes"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
@@ -3647,6 +4088,9 @@ msgstr ""
msgid "This project"
msgstr ""
+msgid "This project does not belong to a group and can therefore not make use of group Runners."
+msgstr ""
+
msgid "This repository"
msgstr ""
@@ -3659,6 +4103,12 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
+msgid "Time remaining"
+msgstr ""
+
+msgid "Time spent"
+msgstr ""
+
msgid "Time tracking"
msgstr ""
@@ -3825,12 +4275,21 @@ msgstr ""
msgid "To import an SVN repository, check out %{svn_link}."
msgstr ""
+msgid "To start serving your jobs you can add Runners to your group"
+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 "Todo"
msgstr ""
+msgid "Toggle Sidebar"
+msgstr ""
+
+msgid "Toggle discussion"
+msgstr ""
+
msgid "Toggle sidebar"
msgstr ""
@@ -3855,6 +4314,12 @@ msgstr ""
msgid "Trigger this manual action"
msgstr ""
+msgid "Try again"
+msgstr ""
+
+msgid "Unable to load the diff. %{button_try_again}"
+msgstr ""
+
msgid "Unlock"
msgstr ""
@@ -3864,6 +4329,21 @@ msgstr ""
msgid "Unresolve discussion"
msgstr ""
+msgid "Unstage all"
+msgstr ""
+
+msgid "Unstage changes"
+msgstr ""
+
+msgid "Unstaged"
+msgstr ""
+
+msgid "Unstaged %{type}"
+msgstr ""
+
+msgid "Unstaged and staged %{type}"
+msgstr ""
+
msgid "Unstar"
msgstr ""
@@ -3918,9 +4398,6 @@ msgstr ""
msgid "Verified"
msgstr ""
-msgid "View and edit lines"
-msgstr ""
-
msgid "View file @ "
msgstr ""
@@ -3972,6 +4449,12 @@ msgstr ""
msgid "Web terminal"
msgstr ""
+msgid "When a runner is locked, it cannot be assigned to other projects"
+msgstr ""
+
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -4032,6 +4515,12 @@ msgstr ""
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
+msgid "WikiPageConfirmDelete|Delete page"
+msgstr ""
+
+msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
+msgstr ""
+
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
msgstr ""
@@ -4086,6 +4575,9 @@ msgstr ""
msgid "Write a commit message..."
msgstr ""
+msgid "Yes"
+msgstr ""
+
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
@@ -4119,12 +4611,21 @@ msgstr ""
msgid "You can only edit files when you are on a branch"
msgstr ""
+msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
+msgstr ""
+
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
+msgid "You have no permissions"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
+msgid "You must have master access to force delete a lock"
+msgstr ""
+
msgid "You must sign in to star a project"
msgstr ""
@@ -4194,6 +4695,9 @@ msgstr ""
msgid "Your projects"
msgstr ""
+msgid "ago"
+msgstr ""
+
msgid "among other things"
msgstr ""
@@ -4217,9 +4721,18 @@ msgstr[1] ""
msgid "deploy token"
msgstr ""
+msgid "disabled"
+msgstr ""
+
+msgid "enabled"
+msgstr ""
+
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr ""
+msgid "for this project"
+msgstr ""
+
msgid "importing"
msgstr ""
@@ -4416,6 +4929,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "remaining"
+msgstr ""
+
msgid "remove due date"
msgstr ""
diff --git a/package.json b/package.json
index af1da9cffa8..13be495b702 100644
--- a/package.json
+++ b/package.json
@@ -5,9 +5,9 @@
"eslint": "eslint --max-warnings 0 --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
- "karma": "karma start --single-run true config/karma.config.js",
+ "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
- "karma-start": "karma start config/karma.config.js",
+ "karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
"prettier-staged": "node ./scripts/frontend/prettier.js",
"prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all",
@@ -16,25 +16,26 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "1.23.0",
+ "@gitlab-org/gitlab-svgs": "^1.23.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
- "babel-core": "^6.26.0",
- "babel-loader": "^7.1.2",
+ "babel-core": "^6.26.3",
+ "babel-loader": "^7.1.4",
"babel-plugin-transform-define": "^1.3.0",
"babel-preset-latest": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"blackst0ne-mermaid": "^7.1.0-fixed",
- "bootstrap-sass": "^3.3.6",
+ "bootstrap": "4.1",
"brace-expansion": "^1.1.8",
+ "cache-loader": "^1.2.2",
"chart.js": "1.0.2",
"classlist-polyfill": "^1.2.0",
"clipboard": "^1.7.1",
- "compression-webpack-plugin": "^1.1.7",
- "copy-webpack-plugin": "^4.4.1",
+ "compression-webpack-plugin": "^1.1.11",
+ "copy-webpack-plugin": "^4.5.1",
"core-js": "^2.4.1",
"cropper": "^2.3.0",
- "css-loader": "^0.28.9",
+ "css-loader": "^0.28.11",
"d3-array": "^1.2.1",
"d3-axis": "^1.0.8",
"d3-brush": "^1.0.4",
@@ -49,7 +50,7 @@
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
"exports-loader": "^0.7.0",
- "file-loader": "^1.1.8",
+ "file-loader": "^1.1.11",
"fuzzaldrin-plus": "^0.5.0",
"glob": "^7.1.2",
"imports-loader": "^0.8.0",
@@ -64,40 +65,47 @@
"marked": "^0.3.12",
"monaco-editor": "0.10.0",
"mousetrap": "^1.4.6",
- "name-all-modules-plugin": "^1.0.1",
"pikaday": "^1.6.1",
+ "popper.js": "^1.14.3",
"prismjs": "^1.6.0",
"raphael": "^2.2.7",
"raven-js": "^3.22.1",
"raw-loader": "^0.5.1",
- "react-dev-utils": "^5.0.0",
"sanitize-html": "^1.16.1",
"select2": "3.5.2-browserify",
+ "sha1": "^1.1.1",
"sql.js": "^0.4.0",
- "style-loader": "^0.20.2",
+ "stickyfilljs": "^2.0.5",
+ "style-loader": "^0.21.0",
"svg4everybody": "2.1.9",
"three": "^0.84.0",
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",
"timeago.js": "^3.0.2",
- "underscore": "^1.8.3",
- "url-loader": "^0.6.2",
+ "underscore": "^1.9.0",
+ "url-loader": "^1.0.1",
"visibilityjs": "^1.2.4",
- "vue": "^2.5.13",
- "vue-loader": "^14.1.1",
- "vue-resource": "^1.3.5",
+ "vue": "^2.5.16",
+ "vue-loader": "^15.2.0",
+ "vue-resource": "^1.5.0",
"vue-router": "^3.0.1",
- "vue-template-compiler": "^2.5.13",
+ "vue-template-compiler": "^2.5.16",
+ "vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1",
- "webpack": "^3.11.0",
- "webpack-bundle-analyzer": "^2.10.0",
- "webpack-stats-plugin": "^0.1.5",
- "worker-loader": "^1.1.0"
+ "webpack": "^4.7.0",
+ "webpack-bundle-analyzer": "^2.11.1",
+ "webpack-cli": "^2.1.2",
+ "webpack-stats-plugin": "^0.2.1",
+ "worker-loader": "^1.1.1"
},
"devDependencies": {
- "axios-mock-adapter": "^1.10.0",
+ "axios-mock-adapter": "^1.15.0",
"babel-eslint": "^8.0.2",
- "babel-plugin-istanbul": "^4.1.5",
+ "babel-plugin-istanbul": "^4.1.6",
+ "babel-plugin-rewire": "^1.1.0",
+ "babel-template": "^6.26.0",
+ "babel-types": "^6.26.0",
+ "chalk": "^2.4.1",
"commander": "^2.15.1",
"eslint": "^3.18.0",
"eslint-config-airbnb-base": "^10.0.1",
@@ -112,15 +120,15 @@
"istanbul": "^0.4.5",
"jasmine-core": "^2.9.0",
"jasmine-jquery": "^2.1.1",
- "karma": "^2.0.0",
+ "karma": "^2.0.2",
"karma-chrome-launcher": "^2.2.0",
- "karma-coverage-istanbul-reporter": "^1.4.1",
+ "karma-coverage-istanbul-reporter": "^1.4.2",
"karma-jasmine": "^1.1.1",
"karma-mocha-reporter": "^2.2.5",
"karma-sourcemap-loader": "^0.3.7",
- "karma-webpack": "2.0.7",
- "nodemon": "^1.15.1",
+ "karma-webpack": "3.0.0",
+ "nodemon": "^1.17.3",
"prettier": "1.11.1",
- "webpack-dev-server": "^2.11.2"
+ "webpack-dev-server": "^3.1.4"
}
}
diff --git a/qa/Dockerfile b/qa/Dockerfile
index ed2ee73bea0..77cee9c5461 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -1,4 +1,4 @@
-FROM ruby:2.4
+FROM ruby:2.4-stretch
LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>"
ENV DEBIAN_FRONTEND noninteractive
diff --git a/qa/Gemfile b/qa/Gemfile
index c3e61568f3d..d69c71003ae 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -6,5 +6,4 @@ gem 'capybara-screenshot', '~> 1.0.18'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.8.0'
-gem 'net-ssh', require: false
gem 'airborne', '~> 0.2.13'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 51d2e4d7a10..1bc424335f8 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -46,9 +46,8 @@ GEM
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.11.1)
- net-ssh (4.1.0)
netrc (0.11.0)
- nokogiri (1.8.1)
+ nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
pry (0.11.3)
coderay (~> 1.1.0)
@@ -98,7 +97,6 @@ DEPENDENCIES
airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
- net-ssh
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
diff --git a/qa/qa.rb b/qa/qa.rb
index fff99a1d31b..40e12c8b336 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -11,9 +11,15 @@ module QA
autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
- autoload :RSAKey, 'qa/runtime/rsa_key'
autoload :Address, 'qa/runtime/address'
autoload :API, 'qa/runtime/api'
+
+ module Key
+ autoload :Base, 'qa/runtime/key/base'
+ autoload :RSA, 'qa/runtime/key/rsa'
+ autoload :ECDSA, 'qa/runtime/key/ecdsa'
+ autoload :ED25519, 'qa/runtime/key/ed25519'
+ end
end
##
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 6e8905cde78..795f1f9cb1a 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -2,7 +2,10 @@ module QA
module Factory
module Repository
class Push < Factory::Base
- attr_writer :file_name, :file_content, :commit_message, :branch_name, :new_branch
+ attr_accessor :file_name, :file_content, :commit_message,
+ :branch_name, :new_branch
+
+ attr_writer :remote_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-code'
@@ -17,23 +20,32 @@ module QA
@new_branch = true
end
+ def remote_branch
+ @remote_branch ||= branch_name
+ end
+
def fabricate!
project.visit!
Git::Repository.perform do |repository|
- repository.location = Page::Project::Show.act do
+ repository.uri = Page::Project::Show.act do
choose_repository_clone_http
- repository_location
+ repository_location.uri
end
repository.use_default_credentials
repository.clone
repository.configure_identity('GitLab QA', 'root@gitlab.com')
- repository.checkout(@branch_name) unless @new_branch
- repository.add_file(@file_name, @file_content)
- repository.commit(@commit_message)
- repository.push_changes(@branch_name)
+ if new_branch
+ repository.checkout_new_branch(branch_name)
+ else
+ repository.checkout(branch_name)
+ end
+
+ repository.add_file(file_name, file_content)
+ repository.commit(commit_message)
+ repository.push_changes("#{branch_name}:#{remote_branch}")
end
end
end
diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb
index d0ef142e90d..1785441f5a8 100644
--- a/qa/qa/factory/resource/branch.rb
+++ b/qa/qa/factory/resource/branch.rb
@@ -2,7 +2,8 @@ module QA
module Factory
module Resource
class Branch < Factory::Base
- attr_accessor :project, :branch_name, :allow_to_push, :protected
+ 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'
@@ -23,6 +24,7 @@ module QA
def initialize
@branch_name = 'test/branch'
@allow_to_push = true
+ @allow_to_merge = true
@protected = false
end
@@ -39,7 +41,9 @@ module QA
resource.project = project
resource.file_name = 'README.md'
resource.commit_message = 'Add readme'
- resource.branch_name = "master:#{@branch_name}"
+ resource.branch_name = 'master'
+ resource.new_branch = false
+ resource.remote_branch = @branch_name
end
Page::Project::Show.act { wait_for_push }
@@ -63,7 +67,22 @@ module QA
page.allow_no_one_to_push
end
+ if allow_to_merge
+ page.allow_devs_and_masters_to_merge
+ else
+ page.allow_no_one_to_merge
+ end
+
+ page.wait(reload: false) do
+ !page.first('.btn-create').disabled?
+ end
+
page.protect_branch
+
+ # Wait for page load, which resets the expanded sections
+ page.wait(reload: false) do
+ !page.has_content?('Collapse')
+ end
end
end
end
diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb
index ff0b4a46b77..ea8a3ad687d 100644
--- a/qa/qa/factory/resource/deploy_key.rb
+++ b/qa/qa/factory/resource/deploy_key.rb
@@ -4,15 +4,15 @@ module QA
class DeployKey < Factory::Base
attr_accessor :title, :key
- product :title do
+ product :fingerprint do |resource|
Page::Project::Settings::Repository.act do
- expand_deploy_keys(&:key_title)
- end
- end
+ expand_deploy_keys do |key|
+ key_offset = key.key_titles.index do |title|
+ title.text == resource.title
+ end
- product :fingerprint do
- Page::Project::Settings::Repository.act do
- expand_deploy_keys(&:key_fingerprint)
+ key.key_fingerprints[key_offset].text
+ end
end
end
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index 539fe6b8a70..7588ac5735d 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -24,12 +24,14 @@ module QA
dependency Factory::Repository::Push, as: :target do |push, factory|
factory.project.visit!
push.project = factory.project
- push.branch_name = "master:#{factory.target_branch}"
+ push.branch_name = 'master'
+ push.remote_branch = factory.target_branch
end
dependency Factory::Repository::Push, as: :source do |push, factory|
push.project = factory.project
- push.branch_name = "#{factory.target_branch}:#{factory.source_branch}"
+ push.branch_name = factory.target_branch
+ push.remote_branch = factory.source_branch
push.file_name = "added_file.txt"
push.file_content = "File Added"
end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index 7df2dc6618c..cda1b35ba6a 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -17,6 +17,13 @@ module QA
Page::Project::Show.act { project_name }
end
+ product :repository_ssh_location do
+ Page::Project::Show.act do
+ choose_repository_clone_ssh
+ repository_location
+ end
+ end
+
def fabricate!
group.visit!
diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb
index c734d739b4a..12a830da116 100644
--- a/qa/qa/factory/resource/secret_variable.rb
+++ b/qa/qa/factory/resource/secret_variable.rb
@@ -16,8 +16,7 @@ module QA
Page::Project::Settings::CICD.perform do |setting|
setting.expand_secret_variables do |page|
- page.fill_variable_key(key)
- page.fill_variable_value(value)
+ page.fill_variable(key, value)
page.save_variables
end
diff --git a/qa/qa/git/location.rb b/qa/qa/git/location.rb
index 30538388530..b74f38f3ae3 100644
--- a/qa/qa/git/location.rb
+++ b/qa/qa/git/location.rb
@@ -14,7 +14,7 @@ module QA
def initialize(git_uri)
@git_uri = git_uri
@uri =
- if git_uri.start_with?('ssh://')
+ if git_uri =~ %r{\A(?:ssh|http|https)://}
URI.parse(git_uri)
else
*rest, path = git_uri.split(':')
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 2f9f06ba277..1367671e3ca 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -15,8 +15,7 @@ module QA
end
end
- def location=(address)
- @location = address
+ def uri=(address)
@uri = URI(address)
end
@@ -43,6 +42,10 @@ module QA
`git checkout "#{branch_name}"`
end
+ def checkout_new_branch(branch_name)
+ `git checkout -b "#{branch_name}"`
+ end
+
def shallow_clone
clone('--depth 1')
end
diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md
index d38223f690d..2dbc59846e7 100644
--- a/qa/qa/page/README.md
+++ b/qa/qa/page/README.md
@@ -115,8 +115,8 @@ from within the `qa` directory.
## Where to ask for help?
-If you need more information, ask for help on `#qa` channel on Slack (GitLab
-Team only).
+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 QA issue tracker.
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index a313d46205d..0a69af88570 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -64,6 +64,10 @@ module QA
find(element_selector_css(name))
end
+ def all_elements(name)
+ all(element_selector_css(name))
+ end
+
def click_element(name)
find_element(name).click
end
diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb
index df93a5fa2d2..644fedecc90 100644
--- a/qa/qa/page/menu/main.rb
+++ b/qa/qa/page/menu/main.rb
@@ -2,12 +2,15 @@ module QA
module Page
module Menu
class Main < Page::Base
+ view 'app/views/layouts/header/_current_user_dropdown.html.haml' do
+ element :user_sign_out_link, 'link_to _("Sign out")'
+ element :settings_link, 'link_to s_("CurrentUser|Settings")'
+ end
+
view 'app/views/layouts/header/_default.html.haml' do
element :navbar
element :user_avatar
- element :user_menu, '.dropdown-menu-nav'
- element :user_sign_out_link, 'link_to "Sign out"'
- element :settings_link, 'link_to "Settings"'
+ element :user_menu, '.dropdown-menu'
end
view 'app/views/layouts/nav/_dashboard.html.haml' do
@@ -67,7 +70,7 @@ module QA
within_top_menu do
click_element :user_avatar
- page.within('.dropdown-menu-nav') do
+ page.within('.dropdown-menu') do
yield
end
end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index 21bda74efb2..83bb224b5c3 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -1,18 +1,28 @@
module QA::Page
module Project::Job
class Show < QA::Page::Base
+ COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running
+ PASSED_STATUS = 'passed'.freeze
+
view 'app/views/projects/jobs/show.html.haml' do
element :build_output, '.js-build-output'
end
- def output
- css = '.js-build-output'
+ view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do
+ element :status_badge, 'ci-status'
+ end
- wait(reload: false) do
- has_css?(css)
- end
+ def completed?
+ COMPLETED_STATUSES.include? find('.ci-status').text
+ end
- find(css).text
+ def passed?
+ find('.ci-status').text == PASSED_STATUS
+ end
+
+ # Reminder: You may wish to wait for a particular job status before checking output
+ def output
+ find('.js-build-output').text
end
end
end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index 332e84724c7..4428e263bbb 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -42,6 +42,18 @@ module QA
end
end
+ def key_titles
+ within_project_deploy_keys do
+ all_elements(:key_title)
+ end
+ end
+
+ def key_fingerprints
+ within_project_deploy_keys do
+ all_elements(:key_fingerprint)
+ end
+ end
+
private
def within_project_deploy_keys
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index f3563401124..63bc3aaa2bc 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -11,6 +11,13 @@ module QA
view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do
element :allowed_to_push_select
element :allowed_to_push_dropdown
+ element :allowed_to_merge_select
+ element :allowed_to_merge_dropdown
+ end
+
+ view 'app/views/projects/protected_branches/_update_protected_branch.html.haml' do
+ element :allowed_to_push
+ element :allowed_to_merge
end
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
@@ -30,11 +37,19 @@ module QA
end
def allow_no_one_to_push
- allow_to_push('No one')
+ click_allow(:push, 'No one')
end
def allow_devs_and_masters_to_push
- allow_to_push('Developers + Masters')
+ click_allow(:push, 'Developers + Masters')
+ end
+
+ def allow_no_one_to_merge
+ click_allow(:merge, 'No one')
+ end
+
+ def allow_devs_and_masters_to_merge
+ click_allow(:merge, 'Developers + Masters')
end
def protect_branch
@@ -55,11 +70,15 @@ module QA
private
- def allow_to_push(text)
- click_element :allowed_to_push_select
+ def click_allow(action, text)
+ click_element :"allowed_to_#{action}_select"
- within_element(:allowed_to_push_dropdown) do
+ within_element(:"allowed_to_#{action}_dropdown") do
click_on text
+
+ wait(reload: false) do
+ has_css?('.is-active')
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/secret_variables.rb
index c95c79f137d..d2f5d5a9060 100644
--- a/qa/qa/page/project/settings/secret_variables.rb
+++ b/qa/qa/page/project/settings/secret_variables.rb
@@ -7,10 +7,8 @@ module QA
view 'app/views/ci/variables/_variable_row.html.haml' do
element :variable_row, '.ci-variable-row-body'
- element :variable_key, '.js-ci-variable-input-key'
- element :variable_value, '.js-ci-variable-input-value'
- element :key_placeholder, 'Input variable key'
- element :value_placeholder, 'Input variable value'
+ element :variable_key, '.qa-ci-variable-input-key'
+ element :variable_value, '.qa-ci-variable-input-value'
end
view 'app/views/ci/variables/_index.html.haml' do
@@ -18,12 +16,14 @@ module QA
element :reveal_values, '.js-secret-value-reveal-button'
end
- def fill_variable_key(key)
- fill_in('Input variable key', with: key, match: :first)
- end
+ def fill_variable(key, value)
+ keys = all_elements(:ci_variable_input_key)
+ index = keys.size - 1
- def fill_variable_value(value)
- fill_in('Input variable value', with: value, match: :first)
+ # After we fill the key, JS would generate another field so
+ # we need to use the same index to find the corresponding one.
+ keys[index].set(key)
+ all_elements(:ci_variable_input_value)[index].set(value)
end
def save_variables
@@ -36,7 +36,7 @@ module QA
def variable_value(key)
within('.ci-variable-row-body', text: key) do
- find('.js-ci-variable-input-value').value
+ find('.qa-ci-variable-input-value').value
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index c7e7ece792d..5bbef040330 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -38,11 +38,7 @@ module QA
end
def repository_location
- find('#project_clone').value
- end
-
- def repository_location_uri
- Git::Location.new(repository_location)
+ Git::Location.new(find('#project_clone').value)
end
def project_name
@@ -91,7 +87,7 @@ module QA
end
# Ensure git clone textbox was updated
- repository_location.include?(detect_text)
+ repository_location.git_uri.include?(detect_text)
end
end
end
diff --git a/qa/qa/runtime/key/base.rb b/qa/qa/runtime/key/base.rb
new file mode 100644
index 00000000000..c7e5ebada7b
--- /dev/null
+++ b/qa/qa/runtime/key/base.rb
@@ -0,0 +1,36 @@
+module QA
+ module Runtime
+ module Key
+ class Base
+ attr_reader :name, :bits, :private_key, :public_key, :fingerprint
+
+ def initialize(name, bits)
+ @name = name
+ @bits = bits
+
+ Dir.mktmpdir do |dir|
+ path = "#{dir}/id_#{name}"
+
+ ssh_keygen(name, bits, path)
+ populate_key_data(path)
+ end
+ end
+
+ private
+
+ def ssh_keygen(name, bits, path)
+ cmd = %W[ssh-keygen -t #{name} -b #{bits} -f #{path} -N] << ''
+
+ Service::Shellout.shell(cmd)
+ end
+
+ def populate_key_data(path)
+ @private_key = File.binread(path)
+ @public_key = File.binread("#{path}.pub")
+ @fingerprint =
+ `ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/key/ecdsa.rb b/qa/qa/runtime/key/ecdsa.rb
new file mode 100644
index 00000000000..20adad45913
--- /dev/null
+++ b/qa/qa/runtime/key/ecdsa.rb
@@ -0,0 +1,12 @@
+# rubocop:disable Naming/FileName
+module QA
+ module Runtime
+ module Key
+ class ECDSA < Base
+ def initialize(bits = 521)
+ super('ecdsa', bits)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/key/ed25519.rb b/qa/qa/runtime/key/ed25519.rb
new file mode 100644
index 00000000000..63865c1cee5
--- /dev/null
+++ b/qa/qa/runtime/key/ed25519.rb
@@ -0,0 +1,12 @@
+# rubocop:disable Naming/FileName
+module QA
+ module Runtime
+ module Key
+ class ED25519 < Base
+ def initialize
+ super('ed25519', 256)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/key/rsa.rb b/qa/qa/runtime/key/rsa.rb
new file mode 100644
index 00000000000..d94bde52325
--- /dev/null
+++ b/qa/qa/runtime/key/rsa.rb
@@ -0,0 +1,11 @@
+module QA
+ module Runtime
+ module Key
+ class RSA < Base
+ def initialize(bits = 4096)
+ super('rsa', bits)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/rsa_key.rb b/qa/qa/runtime/rsa_key.rb
deleted file mode 100644
index fcd7dcc4f02..00000000000
--- a/qa/qa/runtime/rsa_key.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'net/ssh'
-require 'forwardable'
-
-module QA
- module Runtime
- class RSAKey
- extend Forwardable
-
- attr_reader :key
- def_delegators :@key, :fingerprint, :to_pem
-
- def initialize(bits = 4096)
- @key = OpenSSL::PKey::RSA.new(bits)
- end
-
- def public_key
- @public_key ||= "#{key.ssh_type} #{[key.to_blob].pack('m0')}"
- end
- end
- end
-end
diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb
index c87eb5f3dfb..cff320cb751 100644
--- a/qa/qa/scenario/test/sanity/selectors.rb
+++ b/qa/qa/scenario/test/sanity/selectors.rb
@@ -31,7 +31,7 @@ module QA
current changes in this merge request.
For more help see documentation in `qa/page/README.md` file or
- ask for help on #qa channel on Slack (GitLab Team only).
+ ask for help on #quality channel on Slack (GitLab Team only).
If you are not a Team Member, and you still need help to
contribute, please open an issue in GitLab QA issue tracker.
diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb
index 76fb2af6319..1ca9504bb33 100644
--- a/qa/qa/service/shellout.rb
+++ b/qa/qa/service/shellout.rb
@@ -5,6 +5,8 @@ module QA
module Shellout
CommandError = Class.new(StandardError)
+ module_function
+
##
# TODO, make it possible to use generic QA framework classes
# as a library - gitlab-org/gitlab-qa#94
@@ -12,7 +14,7 @@ module QA
def shell(command)
puts "Executing `#{command}`"
- Open3.popen2e(command) do |_in, out, wait|
+ Open3.popen2e(*command) do |_in, out, wait|
out.each { |line| puts line }
if wait.value.exited? && wait.value.exitstatus.nonzero?
diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb
index fbf9a4d17e5..0931e649e24 100644
--- a/qa/qa/specs/features/merge_request/create_spec.rb
+++ b/qa/qa/specs/features/merge_request/create_spec.rb
@@ -11,7 +11,7 @@ module QA
expect(page).to have_content('This is a merge request')
expect(page).to have_content('Great feature')
- expect(page).to have_content('Opened less than a minute ago')
+ expect(page).to have_content(/Opened [\w\s]+ a minute ago/)
end
end
end
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
index b9998dda895..de53613dee1 100644
--- a/qa/qa/specs/features/project/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -4,7 +4,7 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- key = Runtime::RSAKey.new
+ key = Runtime::Key::RSA.new
deploy_key_title = 'deploy key title'
deploy_key_value = key.public_key
@@ -13,7 +13,6 @@ module QA
resource.key = deploy_key_value
end
- expect(deploy_key.title).to eq(deploy_key_title)
expect(deploy_key.fingerprint).to eq(key.fingerprint)
end
end
diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
index 19d3c83758a..442ac312b4d 100644
--- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb
+++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
@@ -2,79 +2,101 @@ require 'digest/sha1'
module QA
feature 'cloning code using a deploy key', :core, :docker do
- let(:runner_name) { "qa-runner-#{Time.now.to_i}" }
- let(:key) { Runtime::RSAKey.new }
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+ end
- given(:project) do
- Factory::Resource::Project.fabricate! do |resource|
+ before(:all) do
+ login
+
+ @runner_name = "qa-runner-#{Time.now.to_i}"
+
+ @project = Factory::Resource::Project.fabricate! do |resource|
resource.name = 'deploy-key-clone-project'
end
- end
- after do
- Service::Runner.new(runner_name).remove!
- end
-
- scenario 'user sets up a deploy key to clone code using pipelines' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.act { sign_in_using_credentials }
+ @repository_location = @project.repository_ssh_location
Factory::Resource::Runner.fabricate! do |resource|
- resource.project = project
- resource.name = runner_name
+ resource.project = @project
+ resource.name = @runner_name
resource.tags = %w[qa docker]
resource.image = 'gitlab/gitlab-runner:ubuntu'
end
- Factory::Resource::DeployKey.fabricate! do |resource|
- resource.project = project
- resource.title = 'deploy key title'
- resource.key = key.public_key
- end
+ Page::Menu::Main.act { sign_out }
+ end
- Factory::Resource::SecretVariable.fabricate! do |resource|
- resource.project = project
- resource.key = 'DEPLOY_KEY'
- resource.value = key.to_pem
- end
+ after(:all) do
+ Service::Runner.new(@runner_name).remove!
+ end
- project.visit!
+ keys = [
+ [Runtime::Key::RSA, 8192],
+ [Runtime::Key::ECDSA, 521],
+ [Runtime::Key::ED25519]
+ ]
- repository_uri = Page::Project::Show.act do
- choose_repository_clone_ssh
- repository_location_uri
- end
+ keys.each do |(key_class, bits)|
+ scenario "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do
+ key = key_class.new(*bits)
- gitlab_ci = <<~YAML
- cat-config:
- script:
- - mkdir -p ~/.ssh
- - ssh-keyscan -p #{repository_uri.port} #{repository_uri.host} >> ~/.ssh/known_hosts
- - eval $(ssh-agent -s)
- - echo "$DEPLOY_KEY" | ssh-add -
- - git clone #{repository_uri.git_uri}
- - sha1sum #{project.name}/.gitlab-ci.yml
- tags:
- - qa
- - docker
- YAML
-
- Factory::Repository::Push.fabricate! do |resource|
- resource.project = project
- resource.file_name = '.gitlab-ci.yml'
- resource.commit_message = 'Add .gitlab-ci.yml'
- resource.file_content = gitlab_ci
- end
+ login
+
+ Factory::Resource::DeployKey.fabricate! do |resource|
+ resource.project = @project
+ resource.title = "deploy key #{key.name}(#{key.bits})"
+ resource.key = key.public_key
+ end
+
+ deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}"
+
+ Factory::Resource::SecretVariable.fabricate! do |resource|
+ resource.project = @project
+ resource.key = deploy_key_name
+ resource.value = key.private_key
+ end
+
+ gitlab_ci = <<~YAML
+ cat-config:
+ script:
+ - mkdir -p ~/.ssh
+ - ssh-keyscan -p #{@repository_location.port} #{@repository_location.host} >> ~/.ssh/known_hosts
+ - eval $(ssh-agent -s)
+ - ssh-add -D
+ - echo "$#{deploy_key_name}" | ssh-add -
+ - git clone #{@repository_location.git_uri}
+ - cd #{@project.name}
+ - git checkout #{deploy_key_name}
+ - sha1sum .gitlab-ci.yml
+ tags:
+ - qa
+ - docker
+ YAML
+
+ Factory::Repository::Push.fabricate! do |resource|
+ resource.project = @project
+ resource.file_name = '.gitlab-ci.yml'
+ resource.commit_message = 'Add .gitlab-ci.yml'
+ resource.file_content = gitlab_ci
+ resource.branch_name = deploy_key_name
+ resource.new_branch = true
+ end
+
+ sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
- sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
+ Page::Project::Show.act { wait_for_push }
+ Page::Menu::Side.act { click_ci_cd_pipelines }
+ Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
+ Page::Project::Pipeline::Show.act { go_to_first_job }
- Page::Project::Show.act { wait_for_push }
- Page::Menu::Side.act { click_ci_cd_pipelines }
- Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
- Page::Project::Pipeline::Show.act { go_to_first_job }
+ Page::Project::Job::Show.perform do |job|
+ job.wait(reload: false) { job.completed? }
- Page::Project::Job::Show.perform do |job|
- expect(job.output).to include(sha1sum)
+ expect(job.passed?).to be_truthy, "Job status did not become \"passed\"."
+ expect(job.output).to include(sha1sum)
+ end
end
end
end
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index 2adb7524a46..bc9eb57bdb4 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -18,7 +18,7 @@ module QA
end
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act do
@@ -33,7 +33,7 @@ module QA
scenario 'user performs a deep clone' do
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act { clone }
@@ -44,7 +44,7 @@ module QA
scenario 'user performs a shallow clone' do
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act { shallow_clone }
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
index 88fa4994e32..406b2772b64 100644
--- a/qa/qa/specs/features/repository/protected_branches_spec.rb
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -19,6 +19,13 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
end
+ after do
+ # We need to clear localStorage because we're using it for the dropdown,
+ # and capybara doesn't do this for us.
+ # https://github.com/teamcapybara/capybara/issues/1702
+ Capybara.execute_script 'localStorage.clear()'
+ end
+
scenario 'user is able to protect a branch' do
protected_branch = Factory::Resource::Branch.fabricate! do |resource|
resource.branch_name = branch_name
@@ -42,7 +49,7 @@ module QA
project.visit!
Git::Repository.perform do |repository|
- repository.location = location
+ repository.uri = location.uri
repository.use_default_credentials
repository.act do
diff --git a/qa/spec/runtime/key/ecdsa_spec.rb b/qa/spec/runtime/key/ecdsa_spec.rb
new file mode 100644
index 00000000000..8951e82b9bb
--- /dev/null
+++ b/qa/spec/runtime/key/ecdsa_spec.rb
@@ -0,0 +1,18 @@
+describe QA::Runtime::Key::ECDSA do
+ describe '#public_key' do
+ [256, 384, 521].each do |bits|
+ it "generates a public #{bits}-bits ECDSA key" do
+ subject = described_class.new(bits).public_key
+
+ expect(subject).to match(%r{\Aecdsa\-sha2\-\w+ AAAA[0-9A-Za-z+/]+={0,3}})
+ end
+ end
+ end
+
+ describe '#new' do
+ it 'does not support arbitrary bits' do
+ expect { described_class.new(123) }
+ .to raise_error(QA::Service::Shellout::CommandError)
+ end
+ end
+end
diff --git a/qa/spec/runtime/key/ed25519_spec.rb b/qa/spec/runtime/key/ed25519_spec.rb
new file mode 100644
index 00000000000..4844e7affdf
--- /dev/null
+++ b/qa/spec/runtime/key/ed25519_spec.rb
@@ -0,0 +1,9 @@
+describe QA::Runtime::Key::ED25519 do
+ describe '#public_key' do
+ subject { described_class.new.public_key }
+
+ it 'generates a public ED25519 key' do
+ expect(subject).to match(%r{\Assh\-ed25519 AAAA[0-9A-Za-z+/]})
+ end
+ end
+end
diff --git a/qa/spec/runtime/key/rsa_spec.rb b/qa/spec/runtime/key/rsa_spec.rb
new file mode 100644
index 00000000000..fbcc7ffdcb4
--- /dev/null
+++ b/qa/spec/runtime/key/rsa_spec.rb
@@ -0,0 +1,9 @@
+describe QA::Runtime::Key::RSA do
+ describe '#public_key' do
+ subject { described_class.new.public_key }
+
+ it 'generates a public RSA key' do
+ expect(subject).to match(%r{\Assh\-rsa AAAA[0-9A-Za-z+/]+={0,3}})
+ end
+ end
+end
diff --git a/qa/spec/runtime/rsa_key.rb b/qa/spec/runtime/rsa_key.rb
deleted file mode 100644
index 6d7ab4dcd2e..00000000000
--- a/qa/spec/runtime/rsa_key.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-describe QA::Runtime::RSAKey do
- describe '#public_key' do
- subject { described_class.new.public_key }
-
- it 'generates a public RSA key' do
- expect(subject).to match(%r{\Assh\-rsa AAAA[0-9A-Za-z+/]+={0,3}\z})
- end
- end
-end
diff --git a/rubocop/cop/gitlab/has_many_through_scope.rb b/rubocop/cop/gitlab/has_many_through_scope.rb
deleted file mode 100644
index 770a2a0529f..00000000000
--- a/rubocop/cop/gitlab/has_many_through_scope.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'gitlab/styles/rubocop/model_helpers'
-
-module RuboCop
- module Cop
- module Gitlab
- class HasManyThroughScope < RuboCop::Cop::Cop
- include ::Gitlab::Styles::Rubocop::ModelHelpers
-
- MSG = 'Always provide an explicit scope calling auto_include(false) when using has_many :through'.freeze
-
- def_node_search :through?, <<~PATTERN
- (pair (sym :through) _)
- PATTERN
-
- def_node_matcher :has_many_through?, <<~PATTERN
- (send nil? :has_many ... #through?)
- PATTERN
-
- def_node_search :disables_auto_include?, <<~PATTERN
- (send _ :auto_include false)
- PATTERN
-
- def_node_matcher :scope_disables_auto_include?, <<~PATTERN
- (block (send nil? :lambda) _ #disables_auto_include?)
- PATTERN
-
- def on_send(node)
- return unless in_model?(node)
- return unless has_many_through?(node)
-
- target = node
- scope_argument = node.children[3]
-
- if scope_argument.children[0].children.last == :lambda
- return if scope_disables_auto_include?(scope_argument)
-
- target = scope_argument
- end
-
- add_offense(target, location: :expression)
- end
- end
- end
- end
-end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 96061672749..f05990232ab 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,8 +1,7 @@
# rubocop:disable Naming/FileName
-require_relative 'cop/gitlab/has_many_through_scope'
-require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
+require_relative 'cop/gitlab/httparty'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize'
diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb
index 6c0f0193b1a..9bf5f1e3b18 100644
--- a/rubocop/spec_helpers.rb
+++ b/rubocop/spec_helpers.rb
@@ -1,6 +1,6 @@
module RuboCop
module SpecHelpers
- SPEC_HELPERS = %w[spec_helper.rb rails_helper.rb].freeze
+ SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze
# Returns true if the given node originated from the spec directory.
def in_spec?(node)
diff --git a/scripts/create_mysql_user.sh b/scripts/create_mysql_user.sh
index 286b1325f1d..35f68c581f3 100644
--- a/scripts/create_mysql_user.sh
+++ b/scripts/create_mysql_user.sh
@@ -1,7 +1,6 @@
#!/bin/bash
mysql --user=root --host=mysql <<EOF
-CREATE DATABASE IF NOT EXISTS gitlabhq_test DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh
index 8a744df3226..8a049bcd7fb 100644
--- a/scripts/create_postgres_user.sh
+++ b/scripts/create_postgres_user.sh
@@ -1,8 +1,6 @@
#!/bin/bash
psql -h postgres -U postgres postgres <<EOF
-DROP DATABASE IF EXISTS gitlabhq_test;
-CREATE DATABASE gitlabhq_test;
CREATE USER gitlab;
-GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO gitlab;
EOF
diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build
index b42ae2a2595..374401caf89 100755
--- a/scripts/gitaly-test-build
+++ b/scripts/gitaly-test-build
@@ -2,28 +2,29 @@
require 'fileutils'
+require_relative 'gitaly_test'
+
# This script assumes tmp/tests/gitaly already contains the correct
# Gitaly version. We just have to compile it and run its 'bundle
-# install'. We have this separate script for that because weird things
-# were happening in CI when we have a 'bundle exec' process that later
-# called 'bundle install' using a different Gemfile, as happens with
-# gitlab-ce and gitaly.
+# install'. We have this separate script for that to avoid bundle
+# poisoning in CI. This script should only be run in CI.
+class GitalyTestBuild
+ include GitalyTest
-tmp_tests_gitaly_dir = File.expand_path('../tmp/tests/gitaly', __dir__)
+ def run
+ abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
-# Use the top-level bundle vendor folder so that we don't reinstall gems twice
-bundle_vendor_path = File.expand_path('../vendor', __dir__)
+ check_gitaly_config!
-env = {
- # This ensure the `clean` config set in `scripts/prepare_build.sh` isn't taken into account
- 'BUNDLE_IGNORE_CONFIG' => 'true',
- 'BUNDLE_GEMFILE' => File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile'),
- 'BUNDLE_FLAGS' => "--jobs=4 --path=#{bundle_vendor_path} --retry=3"
-}
+ # Starting gitaly further validates its configuration
+ pid = start_gitaly
+ Process.kill('TERM', pid)
-abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
+ # Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'.
+ # Without this a gitaly executable created in the setup-test-env job
+ # will look stale compared to GITALY_SERVER_VERSION.
+ FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24))
+ end
+end
-# Make the 'gitaly' executable look newer than 'GITALY_SERVER_VERSION'.
-# Without this a gitaly executable created in the setup-test-env job
-# will look stale compared to GITALY_SERVER_VERSION.
-FileUtils.touch(File.join(tmp_tests_gitaly_dir, 'gitaly'), mtime: Time.now + (1 << 24))
+GitalyTestBuild.new.run
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index ecb68c6acc6..e9f91f75650 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -1,9 +1,23 @@
#!/usr/bin/env ruby
-gitaly_dir = 'tmp/tests/gitaly'
-env = { 'HOME' => File.expand_path('tmp/tests'),
- 'GEM_PATH' => Gem.path.join(':') }
-args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml]
+# This script is used both in CI and in local development 'rspec' runs.
-# Print the PID of the spawned process
-puts spawn(env, *args, [:out, :err] => 'log/gitaly-test.log')
+require_relative 'gitaly_test'
+
+class GitalyTestSpawn
+ include GitalyTest
+
+ def run
+ check_gitaly_config!
+
+ # # Uncomment line below to see all gitaly logs merged into CI trace
+ # spawn('sleep 1; tail -f log/gitaly-test.log')
+
+ pid = start_gitaly
+
+ # In local development this pid file is used by rspec.
+ IO.write(File.expand_path('../tmp/tests/gitaly.pid', __dir__), pid)
+ end
+end
+
+GitalyTestSpawn.new.run
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
new file mode 100644
index 00000000000..dee4c2eba7e
--- /dev/null
+++ b/scripts/gitaly_test.rb
@@ -0,0 +1,97 @@
+# This file contains environment settings for gitaly when it's running
+# as part of the gitlab-ce/ee test suite.
+#
+# Please be careful when modifying this file. Your changes must work
+# both for local development rspec runs, and in CI.
+
+require 'socket'
+
+module GitalyTest
+ def tmp_tests_gitaly_dir
+ File.expand_path('../tmp/tests/gitaly', __dir__)
+ end
+
+ def gemfile
+ File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile')
+ end
+
+ def env
+ env_hash = {
+ 'HOME' => File.expand_path('tmp/tests'),
+ 'GEM_PATH' => Gem.path.join(':'),
+ 'BUNDLE_APP_CONFIG' => File.join(File.dirname(gemfile), '.bundle/config'),
+ 'BUNDLE_FLAGS' => "--jobs=4 --retry=3",
+ 'BUNDLE_INSTALL_FLAGS' => nil,
+ 'BUNDLE_GEMFILE' => gemfile,
+ 'RUBYOPT' => nil
+ }
+
+ if ENV['CI']
+ bundle_path = File.expand_path('../vendor/gitaly-ruby', __dir__)
+ env_hash['BUNDLE_FLAGS'] << " --path=#{bundle_path}"
+ end
+
+ env_hash
+ end
+
+ def config_path
+ File.join(tmp_tests_gitaly_dir, 'config.toml')
+ end
+
+ def start_gitaly
+ args = %W[#{tmp_tests_gitaly_dir}/gitaly #{config_path}]
+ pid = spawn(env, *args, [:out, :err] => 'log/gitaly-test.log')
+
+ begin
+ try_connect!
+ rescue
+ Process.kill('TERM', pid)
+ raise
+ end
+
+ pid
+ end
+
+ def check_gitaly_config!
+ puts 'Checking gitaly-ruby bundle...'
+ abort 'bundle check failed' unless system(env, 'bundle', 'check', chdir: File.dirname(gemfile))
+ end
+
+ def read_socket_path
+ # This code needs to work in an environment where we cannot use bundler,
+ # so we cannot easily use the toml-rb gem. This ad-hoc parser should be
+ # good enough.
+ config_text = IO.read(config_path)
+
+ config_text.lines.each do |line|
+ match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/)
+
+ return match_data[1] if match_data
+ end
+
+ raise "failed to find socket_path in #{config_path}"
+ end
+
+ def try_connect!
+ print "Trying to connect to gitaly: "
+ timeout = 20
+ delay = 0.1
+ socket = read_socket_path
+
+ Integer(timeout / delay).times do
+ begin
+ UNIXSocket.new(socket)
+ puts ' OK'
+
+ return
+ rescue Errno::ENOENT, Errno::ECONNREFUSED
+ print '.'
+ sleep delay
+ end
+ end
+
+ puts ' FAILED'
+
+ raise "could not connect to #{socket}"
+ end
+end
diff --git a/scripts/no-ee-check b/scripts/no-ee-check
new file mode 100755
index 00000000000..29d319dc822
--- /dev/null
+++ b/scripts/no-ee-check
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+ee_path = File.join(File.expand_path(__dir__), '../ee')
+
+if Dir.exist?(ee_path)
+ puts 'The repository contains /ee directory. There should be no /ee directory in CE repo.'
+ exit 1
+end
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 206d62dbc78..75a3cea0448 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -11,7 +11,7 @@ fi
# Only install knapsack after bundle install! Otherwise oddly some native
# gems could not be found under some circumstance. No idea why, hours wasted.
-retry gem install knapsack
+retry gem install knapsack --no-ri --no-rdoc
cp config/gitlab.yml.example config/gitlab.yml
sed -i 's/bin_path: \/usr\/bin\/git/bin_path: \/usr\/local\/bin\/git/' config/gitlab.yml
@@ -49,20 +49,8 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml
-# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
-# user but not necessarily a full schema loaded
-if [ "$CREATE_DB_USER" != "false" ]; then
- if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
- . scripts/create_postgres_user.sh
- else
- . scripts/create_mysql_user.sh
- fi
-fi
-
if [ "$SETUP_DB" != "false" ]; then
- bundle exec rake db:drop db:create db:schema:load db:migrate
-
- if [ "$GITLAB_DATABASE" = "mysql" ]; then
- bundle exec rake add_limits_mysql
- fi
+ setup_db
+elif getent hosts postgres || getent hosts mysql; then
+ setup_db_user_only
fi
diff --git a/scripts/trigger-build-cloud-native b/scripts/trigger-build-cloud-native
new file mode 100755
index 00000000000..b6ca75a588d
--- /dev/null
+++ b/scripts/trigger-build-cloud-native
@@ -0,0 +1,61 @@
+#!/usr/bin/env ruby
+
+require 'gitlab'
+
+#
+# Configure credentials to be used with gitlab gem
+#
+Gitlab.configure do |config|
+ config.endpoint = 'https://gitlab.com/api/v4'
+end
+
+#
+# The remote project
+#
+GITLAB_CNG_REPO = 'gitlab-org/build/CNG'.freeze
+
+def ee?
+ ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md')
+end
+
+def read_file_version(filename)
+ raw_version = File.read(filename).strip
+
+ # if the version matches semver format, treat it as a tag and prepend `v`
+ if raw_version =~ Regexp.compile(/^\d+\.\d+\.\d+(-rc\d+)?(-ee)?$/)
+ "v#{raw_version}"
+ else
+ raw_version
+ end
+end
+
+def params
+ params = {
+ 'GITLAB_SHELL_VERSION' => read_file_version('GITLAB_SHELL_VERSION'),
+ 'GITALY_VERSION' => read_file_version('GITALY_SERVER_VERSION'),
+ 'TRIGGERED_USER' => ENV['GITLAB_USER_NAME'],
+ 'TRIGGER_SOURCE' => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ }
+
+ if ee?
+ params['EE_PIPELINE'] = 'true'
+ params['GITLAB_EE_VERSION'] = ENV['CI_COMMIT_REF_NAME']
+ else
+ params['CE_PIPELINE'] = 'true'
+ params['GITLAB_CE_VERSION'] = ENV['CI_COMMIT_REF_NAME']
+ end
+
+ params
+end
+
+#
+# Trigger a pipeline
+#
+def trigger_pipeline
+ # Create the cross project pipeline using CI_JOB_TOKEN
+ pipeline = Gitlab.run_trigger(GITLAB_CNG_REPO, ENV['CI_JOB_TOKEN'], 'master', params)
+
+ puts "Triggered https://gitlab.com/#{GITLAB_CNG_REPO}/pipelines/#{pipeline.id}"
+end
+
+trigger_pipeline
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 6faa701f0ce..2d2ba115563 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -12,3 +12,21 @@ retry() {
done
return 1
}
+
+setup_db_user_only() {
+ if [ "$GITLAB_DATABASE" = "postgresql" ]; then
+ . scripts/create_postgres_user.sh
+ else
+ . scripts/create_mysql_user.sh
+ fi
+}
+
+setup_db() {
+ setup_db_user_only
+
+ bundle exec rake db:drop db:create db:schema:load db:migrate
+
+ if [ "$GITLAB_DATABASE" = "mysql" ]; then
+ bundle exec rake add_limits_mysql
+ fi
+}
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index fe95d1ef9cd..f0caac40afd 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ApplicationController do
+ include TermsHelper
+
let(:user) { create(:user) }
describe '#check_password_expiration' do
@@ -406,4 +408,65 @@ describe ApplicationController do
end
end
end
+
+ context 'terms' do
+ controller(described_class) do
+ def index
+ render text: 'authenticated'
+ end
+ end
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ sign_in user
+ end
+
+ it 'does not query more when terms are enforced' do
+ control = ActiveRecord::QueryRecorder.new { get :index }
+
+ enforce_terms
+
+ expect { get :index }.not_to exceed_query_limit(control)
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ it 'redirects if the user did not accept the terms' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'does not redirect when the user accepted terms' do
+ accept_terms(user)
+
+ get :index
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ context 'for sessionless users' do
+ before do
+ sign_out user
+ end
+
+ it 'renders a 403 when the sessionless user did not accept the terms' do
+ get :index, rss_token: user.rss_token, format: :atom
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'renders a 200 when the sessionless user accepted the terms' do
+ accept_terms(user)
+
+ get :index, rss_token: user.rss_token, format: :atom
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/concerns/continue_params_spec.rb b/spec/controllers/concerns/continue_params_spec.rb
new file mode 100644
index 00000000000..e2f683ae393
--- /dev/null
+++ b/spec/controllers/concerns/continue_params_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe ContinueParams do
+ let(:controller_class) do
+ Class.new(ActionController::Base) do
+ include ContinueParams
+
+ def request
+ @request ||= Struct.new(:host, :port).new('test.host', 80)
+ end
+ end
+ end
+ subject(:controller) { controller_class.new }
+
+ def strong_continue_params(params)
+ ActionController::Parameters.new(continue: params)
+ end
+
+ it 'cleans up any params that are not allowed' do
+ allow(controller).to receive(:params) do
+ strong_continue_params(to: '/hello',
+ notice: 'world',
+ notice_now: '!',
+ something: 'else')
+ end
+
+ expect(controller.continue_params.keys).to contain_exactly(*%w(to notice notice_now))
+ end
+
+ it 'does not allow cross host redirection' do
+ allow(controller).to receive(:params) do
+ strong_continue_params(to: '//example.com')
+ end
+
+ expect(controller.continue_params[:to]).to be_nil
+ end
+
+ it 'allows redirecting to a path with querystring' do
+ allow(controller).to receive(:params) do
+ strong_continue_params(to: '/hello/world?query=string')
+ end
+
+ expect(controller.continue_params[:to]).to eq('/hello/world?query=string')
+ end
+end
diff --git a/spec/controllers/concerns/internal_redirect_spec.rb b/spec/controllers/concerns/internal_redirect_spec.rb
new file mode 100644
index 00000000000..a0ee13b2352
--- /dev/null
+++ b/spec/controllers/concerns/internal_redirect_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe InternalRedirect do
+ let(:controller_class) do
+ Class.new do
+ include InternalRedirect
+
+ def request
+ @request ||= Struct.new(:host, :port).new('test.host', 80)
+ end
+ end
+ end
+ subject(:controller) { controller_class.new }
+
+ describe '#safe_redirect_path' do
+ it 'is `nil` for invalid uris' do
+ expect(controller.safe_redirect_path('Hello world')).to be_nil
+ end
+
+ it 'is `nil` for paths trying to include a host' do
+ expect(controller.safe_redirect_path('//example.com/hello/world')).to be_nil
+ end
+
+ it 'returns the path if it is valid' do
+ expect(controller.safe_redirect_path('/hello/world')).to eq('/hello/world')
+ end
+
+ it 'returns the path with querystring if it is valid' do
+ expect(controller.safe_redirect_path('/hello/world?hello=world#L123'))
+ .to eq('/hello/world?hello=world#L123')
+ end
+ end
+
+ describe '#safe_redirect_path_for_url' do
+ it 'is `nil` for invalid urls' do
+ expect(controller.safe_redirect_path_for_url('Hello world')).to be_nil
+ end
+
+ it 'is `nil` for urls from a with a different host' do
+ expect(controller.safe_redirect_path_for_url('http://example.com/hello/world')).to be_nil
+ end
+
+ it 'is `nil` for urls from a with a different port' do
+ expect(controller.safe_redirect_path_for_url('http://test.host:3000/hello/world')).to be_nil
+ end
+
+ it 'returns the path if the url is on the same host' do
+ expect(controller.safe_redirect_path_for_url('http://test.host/hello/world')).to eq('/hello/world')
+ end
+
+ it 'returns the path including querystring if the url is on the same host' do
+ expect(controller.safe_redirect_path_for_url('http://test.host/hello/world?hello=world#L123'))
+ .to eq('/hello/world?hello=world#L123')
+ end
+ end
+
+ describe '#host_allowed?' do
+ it 'allows uris with the same host and port' do
+ expect(controller.host_allowed?(URI('http://test.host/test'))).to be(true)
+ end
+
+ it 'rejects uris with other host and port' do
+ expect(controller.host_allowed?(URI('http://example.com/test'))).to be(false)
+ end
+ end
+end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index f4c99ea4064..58bb91a0c80 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -51,6 +51,21 @@ describe SendFileUpload do
end
end
+ context 'with attachment' do
+ subject { controller.send_upload(uploader, attachment: 'test.js') }
+
+ it 'sends a file with content-type of text/plain' do
+ expected_params = {
+ content_type: 'text/plain',
+ filename: 'test.js',
+ disposition: 'attachment'
+ }
+ expect(controller).to receive(:send_file).with(uploader.path, expected_params)
+
+ subject
+ end
+ end
+
context 'when remote file is used' do
before do
stub_uploads_object_storage(uploader: uploader_class)
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
new file mode 100644
index 00000000000..6d31b0ce959
--- /dev/null
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Groups::RunnersController do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner) }
+
+ let(:params) do
+ {
+ group_id: group,
+ id: runner
+ }
+ end
+
+ before do
+ sign_in(user)
+ group.add_master(user)
+ group.runners << runner
+ end
+
+ describe '#update' do
+ it 'updates the runner and ticks the queue' do
+ new_desc = runner.description.swapcase
+
+ expect do
+ post :update, params.merge(runner: { description: new_desc } )
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(runner.description).to eq(new_desc)
+ end
+ end
+
+ describe '#destroy' do
+ it 'destroys the runner' do
+ delete :destroy, params
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(Ci::Runner.find_by(id: runner.id)).to be_nil
+ end
+ end
+
+ describe '#resume' do
+ it 'marks the runner as active and ticks the queue' do
+ runner.update(active: false)
+
+ expect do
+ post :resume, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(runner.active).to eq(true)
+ end
+ end
+
+ describe '#pause' do
+ it 'marks the runner as inactive and ticks the queue' do
+ runner.update(active: true)
+
+ expect do
+ post :pause, params
+ end.to change { runner.ensure_runner_queue_value }
+
+ runner.reload
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(runner.active).to eq(false)
+ end
+ end
+end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 2be46049aab..be49b92d23f 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -223,11 +223,12 @@ describe Import::BitbucketController do
end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do
- let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
before do
+ parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
@@ -273,7 +274,7 @@ describe Import::BitbucketController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index e958be077c2..742f4787126 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -196,10 +196,11 @@ describe Import::GitlabController do
end
context 'user has chosen an existing nested namespace for the project', :postgresql do
- let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
before do
+ parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
@@ -245,7 +246,7 @@ describe Import::GitlabController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
new file mode 100644
index 00000000000..87c10a86cdd
--- /dev/null
+++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Ldap::OmniauthCallbacksController do
+ include_context 'Ldap::OmniauthCallbacksController'
+
+ it 'allows sign in' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+
+ it 'respects remember me checkbox' do
+ expect do
+ post provider, remember_me: '1'
+ end.to change { user.reload.remember_created_at }.from(nil)
+ end
+
+ context 'with 2FA' do
+ let(:user) { create(:omniauth_user, :two_factor_via_otp, extern_uid: uid, provider: provider) }
+
+ it 'passes remember_me to the Devise view' do
+ post provider, remember_me: '1'
+
+ expect(assigns[:user].remember_me).to eq '1'
+ end
+ end
+
+ context 'access denied' do
+ let(:valid_login?) { false }
+
+ it 'warns the user' do
+ post provider
+
+ expect(flash[:alert]).to match(/Access denied for your LDAP account*/)
+ end
+
+ it "doesn't authenticate user" do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'sign up' do
+ let(:user) { double(email: 'new@example.com') }
+
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
+
+ it 'is allowed' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+end
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index de6ef919221..c621eb69171 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -125,7 +125,7 @@ describe ProfilesController, :request_store do
user.reload
expect(response.status).to eq(302)
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{new_username}/#{project.path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_username}/#{project.path}.git")).to be_truthy
end
end
@@ -143,7 +143,7 @@ describe ProfilesController, :request_store do
user.reload
expect(response.status).to eq(302)
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
expect(before_disk_path).to eq(project.disk_path)
end
end
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
index e14ba29fa70..715bb9f5e52 100644
--- a/spec/controllers/projects/clusters/gcp_controller_spec.rb
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -142,7 +142,7 @@ describe Projects::Clusters::GcpController do
context 'when google project billing is enabled' do
before do
- redis_double = double
+ redis_double = double.as_null_object
allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true')
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 694c64ae1ad..003fec8ac68 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -79,41 +79,18 @@ describe Projects::CommitController do
end
describe "as diff" do
- include_examples "export as", :diff
- let(:format) { :diff }
+ it "triggers workhorse to serve the request" do
+ go(id: commit.id, format: :diff)
- it "should really only be a git diff" do
- go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format)
-
- expect(response.body).to start_with("diff --git")
- end
-
- it "is only be a git diff without whitespace changes" do
- go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1)
-
- expect(response.body).to start_with("diff --git")
-
- # without whitespace option, there are more than 2 diff_splits for other formats
- diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n")
- expect(diff_splits.length).to be <= 2
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
end
describe "as patch" do
- include_examples "export as", :patch
- let(:format) { :patch }
- let(:commit2) { project.commit('498214de67004b1da3d820901307bed2a68a8ef6') }
-
- it "is a git email patch" do
- go(id: commit2.id, format: format)
-
- expect(response.body).to start_with("From #{commit2.id}")
- end
-
it "contains a git diff" do
- go(id: commit2.id, format: format)
+ go(id: commit.id, format: :patch)
- expect(response.body).to match(/^diff --git/)
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
end
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 046ce027965..b15cde4314e 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -3,96 +3,99 @@ require 'spec_helper'
describe Projects::CompareController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:ref_from) { "improve%2Fawesome" }
- let(:ref_to) { "feature" }
before do
sign_in(user)
project.add_master(user)
end
- it 'compare shows some diffs' do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- from: ref_from,
- to: ref_to)
+ describe 'GET index' do
+ render_views
+
+ before do
+ get :index, namespace_id: project.namespace, project_id: project
+ end
- expect(response).to be_success
- expect(assigns(:diffs).diff_files.first).not_to be_nil
- expect(assigns(:commits).length).to be >= 1
+ it 'returns successfully' do
+ expect(response).to be_success
+ end
end
- it 'compare shows some diffs with ignore whitespace change option' do
- get(:show,
+ describe 'GET show' do
+ render_views
+
+ subject(:show_request) { get :show, request_params }
+
+ let(:request_params) do
+ {
namespace_id: project.namespace,
project_id: project,
- from: '08f22f25',
- to: '66eceea0',
- w: 1)
-
- expect(response).to be_success
- diff_file = assigns(:diffs).diff_files.first
- expect(diff_file).not_to be_nil
- expect(assigns(:commits).length).to be >= 1
- # without whitespace option, there are more than 2 diff_splits
- diff_splits = diff_file.diff.diff.split("\n")
- expect(diff_splits.length).to be <= 2
- end
+ from: source_ref,
+ to: target_ref,
+ w: whitespace
+ }
+ end
- describe 'non-existent refs' do
- it 'uses invalid source ref' do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- from: 'non-existent',
- to: ref_to)
+ let(:whitespace) { nil }
- expect(response).to be_success
- expect(assigns(:diffs).diff_files.to_a).to eq([])
- expect(assigns(:commits)).to eq([])
- end
+ context 'when the refs exist' do
+ context 'when we set the white space param' do
+ let(:source_ref) { "08f22f25" }
+ let(:target_ref) { "66eceea0" }
+ let(:whitespace) { 1 }
- it 'uses invalid target ref' do
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- from: ref_from,
- to: 'non-existent')
+ it 'shows some diffs with ignore whitespace change option' do
+ show_request
- expect(response).to be_success
- expect(assigns(:diffs)).to eq(nil)
- expect(assigns(:commits)).to eq(nil)
- end
+ expect(response).to be_success
+ diff_file = assigns(:diffs).diff_files.first
+ expect(diff_file).not_to be_nil
+ expect(assigns(:commits).length).to be >= 1
+ # without whitespace option, there are more than 2 diff_splits
+ diff_splits = diff_file.diff.diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
+ end
+
+ context 'when we do not set the white space param' do
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
+ let(:whitespace) { nil }
- it 'redirects back to index when params[:from] is empty and preserves params[:to]' do
- post(:create,
- namespace_id: project.namespace,
- project_id: project,
- from: '',
- to: 'master')
+ it 'sets the diffs and commits ivars' do
+ show_request
- expect(response).to redirect_to(project_compare_index_path(project, to: 'master'))
+ expect(response).to be_success
+ expect(assigns(:diffs).diff_files.first).not_to be_nil
+ expect(assigns(:commits).length).to be >= 1
+ end
+ end
end
- it 'redirects back to index when params[:to] is empty and preserves params[:from]' do
- post(:create,
- namespace_id: project.namespace,
- project_id: project,
- from: 'master',
- to: '')
+ context 'when the source ref does not exist' do
+ let(:source_ref) { 'non-existent-source-ref' }
+ let(:target_ref) { "feature" }
+
+ it 'sets empty diff and commit ivars' do
+ show_request
- expect(response).to redirect_to(project_compare_index_path(project, from: 'master'))
+ expect(response).to be_success
+ expect(assigns(:diffs).diff_files.to_a).to eq([])
+ expect(assigns(:commits)).to eq([])
+ end
end
- it 'redirects back to index when params[:from] and params[:to] are empty' do
- post(:create,
- namespace_id: project.namespace,
- project_id: project,
- from: '',
- to: '')
+ context 'when the target ref does not exist' do
+ let(:target_ref) { 'non-existent-target-ref' }
+ let(:source_ref) { "improve%2Fawesome" }
- expect(response).to redirect_to(namespace_project_compare_index_path)
+ it 'sets empty diff and commit ivars' do
+ show_request
+
+ expect(response).to be_success
+ expect(assigns(:diffs)).to eq([])
+ expect(assigns(:commits)).to eq([])
+ end
end
end
@@ -107,12 +110,14 @@ describe Projects::CompareController do
end
let(:existing_path) { 'files/ruby/feature.rb' }
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
- context 'when the from and to refs exist' do
- context 'when the user has access to the project' do
+ context 'when the source and target refs exist' do
+ context 'when the user has access target the project' do
context 'when the path exists in the diff' do
it 'disables diff notes' do
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path, new_path: existing_path)
expect(assigns(:diff_notes_disabled)).to be_truthy
end
@@ -123,13 +128,13 @@ describe Projects::CompareController do
meth.call(diffs)
end
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path, new_path: existing_path)
end
end
context 'when the path does not exist in the diff' do
before do
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path.succ, new_path: existing_path.succ)
end
it 'returns a 404' do
@@ -138,10 +143,10 @@ describe Projects::CompareController do
end
end
- context 'when the user does not have access to the project' do
+ context 'when the user does not have access target the project' do
before do
project.team.truncate
- diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
@@ -150,9 +155,9 @@ describe Projects::CompareController do
end
end
- context 'when the from ref does not exist' do
+ context 'when the source ref does not exist' do
before do
- diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref.succ, to: target_ref, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
@@ -160,9 +165,9 @@ describe Projects::CompareController do
end
end
- context 'when the to ref does not exist' do
+ context 'when the target ref does not exist' do
before do
- diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path)
+ diff_for_path(from: source_ref, to: target_ref.succ, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
@@ -170,4 +175,153 @@ describe Projects::CompareController do
end
end
end
+
+ describe 'POST create' do
+ subject(:create_request) { post :create, request_params }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ from: source_ref,
+ to: target_ref
+ }
+ end
+
+ context 'when sending valid params' do
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
+
+ it 'redirects back to show' do
+ create_request
+
+ expect(response).to redirect_to(project_compare_path(project, to: target_ref, from: source_ref))
+ end
+ end
+
+ context 'when sending invalid params' do
+ context 'when the source ref is empty and target ref is set' do
+ let(:source_ref) { '' }
+ let(:target_ref) { 'master' }
+
+ it 'redirects back to index and preserves the target ref' do
+ create_request
+
+ expect(response).to redirect_to(project_compare_index_path(project, to: target_ref))
+ end
+ end
+
+ context 'when the target ref is empty and source ref is set' do
+ let(:source_ref) { 'master' }
+ let(:target_ref) { '' }
+
+ it 'redirects back to index and preserves source ref' do
+ create_request
+
+ expect(response).to redirect_to(project_compare_index_path(project, from: source_ref))
+ end
+ end
+
+ context 'when the target and source ref are empty' do
+ let(:source_ref) { '' }
+ let(:target_ref) { '' }
+
+ it 'redirects back to index' do
+ create_request
+
+ expect(response).to redirect_to(namespace_project_compare_index_path)
+ end
+ end
+ end
+ end
+
+ describe 'GET signatures' do
+ subject(:signatures_request) { get :signatures, request_params }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ from: source_ref,
+ to: target_ref,
+ format: :json
+ }
+ end
+
+ context 'when the source and target refs exist' do
+ let(:source_ref) { "improve%2Fawesome" }
+ let(:target_ref) { "feature" }
+
+ context 'when the user has access to the project' do
+ render_views
+
+ let(:signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'signature_commit') }
+ let(:non_signature_commit) { build(:commit, project: project, safe_message: "message", sha: 'non_signature_commit') }
+
+ before do
+ escaped_source_ref = Addressable::URI.unescape(source_ref)
+ escaped_target_ref = Addressable::URI.unescape(target_ref)
+
+ compare_service = CompareService.new(project, escaped_target_ref)
+ compare = compare_service.execute(project, escaped_source_ref)
+
+ expect(CompareService).to receive(:new).with(project, escaped_target_ref).and_return(compare_service)
+ expect(compare_service).to receive(:execute).with(project, escaped_source_ref).and_return(compare)
+
+ expect(compare).to receive(:commits).and_return([signature_commit, non_signature_commit])
+ expect(non_signature_commit).to receive(:has_signature?).and_return(false)
+ end
+
+ it 'returns only the commit with a signature' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(200)
+ parsed_body = JSON.parse(response.body)
+ signatures = parsed_body['signatures']
+
+ expect(signatures.size).to eq(1)
+ expect(signatures.first['commit_sha']).to eq(signature_commit.sha)
+ expect(signatures.first['html']).to be_present
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ before do
+ project.team.truncate
+ end
+
+ it 'returns a 404' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when the source ref does not exist' do
+ let(:source_ref) { 'non-existent-ref-source' }
+ let(:target_ref) { "feature" }
+
+ it 'returns no signatures' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(200)
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body['signatures']).to be_empty
+ end
+ end
+
+ context 'when the target ref does not exist' do
+ let(:target_ref) { 'non-existent-ref-target' }
+ let(:source_ref) { "improve%2Fawesome" }
+
+ it 'returns no signatures' do
+ signatures_request
+
+ expect(response).to have_gitlab_http_status(200)
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body['signatures']).to be_empty
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index c4b32dc3a09..e20623c0ac1 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -4,7 +4,11 @@ describe Projects::ForksController do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user).execute }
- let(:group) { create(:group, owner: forked_project.creator) }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ end
describe 'GET index' do
def get_forks
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index b9a979044fe..a08fcea27a5 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1,7 +1,7 @@
# coding: utf-8
require 'spec_helper'
-describe Projects::JobsController do
+describe Projects::JobsController, :clean_gitlab_redis_shared_state do
include ApiHelpers
include HttpIOHelpers
@@ -10,6 +10,7 @@ describe Projects::JobsController do
let(:user) { create(:user) }
before do
+ stub_feature_flags(ci_enable_live_trace: true)
stub_not_protect_default_branch
end
@@ -489,43 +490,43 @@ describe Projects::JobsController do
id: job.id
end
- context 'when job has a trace artifact' do
+ context "when job has a trace artifact" do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq job.job_artifacts_trace.open.read
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq(job.job_artifacts_trace.open.read)
end
end
- context 'when job has a trace file' do
+ context "when job has a trace file" do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
- it 'send a trace file' do
+ it "send a trace file" do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq 'BUILD TRACE'
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq("BUILD TRACE")
end
end
- context 'when job has a trace in database' do
+ context "when job has a trace in database" do
let(:job) { create(:ci_build, pipeline: pipeline) }
before do
- job.update_column(:trace, 'Sample trace')
+ job.update_column(:trace, "Sample trace")
end
- it 'send a trace file' do
+ it "send a trace file" do
response = subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response.content_type).to eq 'text/plain; charset=utf-8'
- expect(response.body).to eq 'Sample trace'
+ expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
+ expect(response.body).to eq("Sample trace")
end
end
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index 2d7647a6e12..397cc79bde4 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::MergeRequests::ConflictsController do
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_with_conflicts) do
- create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+ create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index 24310b847e8..00d76f3c39a 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -157,34 +157,4 @@ describe Projects::MergeRequests::CreationsController do
expect(response).to have_gitlab_http_status(200)
end
end
-
- describe 'GET #update_branches' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- end
-
- it 'lists the branches of another fork if the user has access' do
- expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true }
-
- get :update_branches,
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- target_project_id: project.id
-
- expect(assigns(:target_branches)).not_to be_empty
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'does not list branches when the user cannot read the project' do
- expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false }
-
- get :update_branches,
- namespace_id: fork_project.namespace,
- project_id: fork_project,
- target_project_id: project.id
-
- expect(response).to have_gitlab_http_status(200)
- expect(assigns(:target_branches)).to eq([])
- end
- end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c8cc6b374f6..d3042be9e8b 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -7,7 +7,7 @@ describe Projects::MergeRequestsController do
let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_with_conflicts) do
- create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+ create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
new file mode 100644
index 00000000000..45c1218a39c
--- /dev/null
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Projects::MirrorsController do
+ include ReactiveCachingHelpers
+
+ describe 'setting up a remote mirror' do
+ set(:project) { create(:project, :repository) }
+
+ context 'when the current project is not a mirror' do
+ it 'allows to create a remote mirror' do
+ sign_in(project.owner)
+
+ expect do
+ do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com' } })
+ end.to change { RemoteMirror.count }.to(1)
+ end
+ end
+ end
+
+ describe '#update' do
+ let(:project) { create(:project, :repository, :remote_mirror) }
+
+ before do
+ sign_in(project.owner)
+ end
+
+ around do |example|
+ Sidekiq::Testing.fake! { example.run }
+ end
+
+ context 'With valid URL for a push' do
+ let(:remote_mirror_attributes) do
+ { "0" => { "enabled" => "0", url: 'https://updated.example.com' } }
+ end
+
+ it 'processes a successful update' do
+ do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
+
+ expect(response).to redirect_to(project_settings_repository_path(project))
+ expect(flash[:notice]).to match(/successfully updated/)
+ end
+
+ it 'should create a RemoteMirror object' do
+ expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1)
+ end
+ end
+
+ context 'With invalid URL for a push' do
+ let(:remote_mirror_attributes) do
+ { "0" => { "enabled" => "0", url: 'ftp://invalid.invalid' } }
+ end
+
+ it 'processes an unsuccessful update' do
+ do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
+
+ expect(response).to redirect_to(project_settings_repository_path(project))
+ expect(flash[:alert]).to match(/must be a valid URL/)
+ end
+
+ it 'should not create a RemoteMirror object' do
+ expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count)
+ end
+ 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
+
+ put :update, attrs
+ end
+end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 35ac999cc65..9e7bc20a6d1 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -35,10 +35,16 @@ describe Projects::PipelinesController do
expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 4
- expect(json_response['count']['all']).to eq 4
- expect(json_response['count']['running']).to eq 1
- expect(json_response['count']['pending']).to eq 1
- expect(json_response['count']['finished']).to eq 1
+ expect(json_response['count']['all']).to eq '4'
+ expect(json_response['count']['running']).to eq '1'
+ expect(json_response['count']['pending']).to eq '1'
+ expect(json_response['count']['finished']).to eq '1'
+ end
+
+ it 'does not include coverage data for the pipelines' do
+ subject
+
+ expect(json_response['pipelines'][0]).not_to include('coverage')
end
context 'when performing gitaly calls', :request_store do
@@ -109,8 +115,7 @@ describe Projects::PipelinesController do
it 'returns html source for stage dropdown' do
expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template('projects/pipelines/_stage')
- expect(json_response).to include('html')
+ expect(response).to match_response_schema('pipeline_stage')
end
end
@@ -133,6 +138,42 @@ describe Projects::PipelinesController do
end
end
+ describe 'GET stages_ajax.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when accessing existing stage' do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'build')
+
+ get_stage_ajax('build')
+ end
+
+ it 'returns html source for stage dropdown' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('projects/pipelines/_stage')
+ expect(json_response).to include('html')
+ end
+ end
+
+ context 'when accessing unknown stage' do
+ before do
+ get_stage_ajax('test')
+ end
+
+ it 'responds with not found' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ def get_stage_ajax(name)
+ get :stage_ajax, namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ stage: name,
+ format: :json
+ end
+ end
+
describe 'GET status.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:status) { pipeline.detailed_status(double('user')) }
diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
index b2b245dba90..871dcf5c796 100644
--- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
@@ -12,44 +12,67 @@ describe Projects::Prometheus::MetricsController do
end
describe 'GET #active_common' do
- before do
- allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter)
- end
+ context 'when prometheus_adapter can query' do
+ before do
+ allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ end
- context 'when prometheus metrics are enabled' do
- context 'when data is not present' do
- before do
- allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({})
- end
+ context 'when prometheus metrics are enabled' do
+ context 'when data is not present' do
+ before do
+ allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({})
+ end
- it 'returns no content response' do
- get :active_common, project_params(format: :json)
+ it 'returns no content response' do
+ get :active_common, project_params(format: :json)
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
+ end
end
- end
- context 'when data is available' do
- let(:sample_response) { { some_data: 1 } }
+ context 'when data is available' do
+ let(:sample_response) { { some_data: 1 } }
+
+ before do
+ allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return(sample_response)
+ end
- before do
- allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return(sample_response)
+ it 'returns no content response' do
+ get :active_common, project_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to eq(sample_response.deep_stringify_keys)
+ end
end
- it 'returns no content response' do
- get :active_common, project_params(format: :json)
+ context 'when requesting non json response' do
+ it 'returns not found response' do
+ get :active_common, project_params
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to eq(sample_response.deep_stringify_keys)
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
+ end
- context 'when requesting non json response' do
- it 'returns not found response' do
- get :active_common, project_params
+ context 'when prometheus_adapter cannot query' do
+ it 'renders 404' do
+ prometheus_adapter = double('prometheus_adapter', can_query?: false)
- expect(response).to have_gitlab_http_status(404)
- end
+ allow(controller).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:query).with(:matched_metrics).and_return({})
+
+ get :active_common, project_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when prometheus_adapter is disabled' do
+ it 'renders 404' do
+ get :active_common, project_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 08e2ccf893a..c3468536ae1 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -54,9 +54,9 @@ describe Projects::RawController do
end
context 'and lfs uses object storage' do
+ let(:lfs_object) { create(:lfs_object, :with_file, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') }
+
before do
- lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
- lfs_object.save!
stub_lfs_object_storage
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index c3b71458e38..a102a3a3c8c 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -40,6 +40,30 @@ describe Projects::RepositoriesController do
expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
+ it 'handles legacy queries with the ref specified as ref in params' do
+ get :archive, namespace_id: project.namespace, project_id: project, ref: 'feature', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
+ it 'handles legacy queries with the ref specified as id in params' do
+ get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
+ it 'prioritizes the id param over the ref param when both are specified' do
+ get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', ref: 'feature_conflict', format: 'zip'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:ref)).to eq('feature')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
+ end
+
context "when the service raises an error" do
before do
allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed")
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index 7dae9b85d78..f1810763d2d 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -17,6 +17,23 @@ describe Projects::Settings::CiCdController do
expect(response).to have_gitlab_http_status(200)
expect(response).to render_template(:show)
end
+
+ context 'with group runners' do
+ let(:group_runner) { create(:ci_runner, runner_type: :group_type) }
+ let(:parent_group) { create(:group) }
+ let(:group) { create(:group, runners: [group_runner], parent: parent_group) }
+ let(:other_project) { create(:project, group: group) }
+ let!(:project_runner) { create(:ci_runner, projects: [other_project], runner_type: :project_type) }
+ let!(:shared_runner) { create(:ci_runner, :shared) }
+
+ it 'sets assignable project runners only' do
+ group.add_master(user)
+
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(assigns(:assignable_runners)).to contain_exactly(project_runner)
+ end
+ end
end
describe '#reset_cache' do
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 55bd4352bd3..555b186fe31 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -265,7 +265,7 @@ describe SessionsController do
it 'redirects correctly for referer on same host with params' do
search_path = '/search?search=seed_project'
allow(controller.request).to receive(:referer)
- .and_return('http://%{host}%{path}' % { host: Gitlab.config.gitlab.host, path: search_path })
+ .and_return('http://%{host}%{path}' % { host: 'test.host', path: search_path })
get(:new, redirect_to_referer: :yes)
diff --git a/spec/controllers/users/terms_controller_spec.rb b/spec/controllers/users/terms_controller_spec.rb
new file mode 100644
index 00000000000..a744463413c
--- /dev/null
+++ b/spec/controllers/users/terms_controller_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+describe Users::TermsController do
+ let(:user) { create(:user) }
+ let(:term) { create(:term) }
+
+ before do
+ sign_in user
+ end
+
+ describe 'GET #index' do
+ it 'redirects when no terms exist' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+
+ it 'shows terms when they exist' do
+ term
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ describe 'POST #accept' do
+ it 'saves that the user accepted the terms' do
+ post :accept, id: term.id
+
+ agreement = user.term_agreements.find_by(term: term)
+
+ expect(agreement.accepted).to eq(true)
+ end
+
+ it 'redirects to a path when specified' do
+ post :accept, id: term.id, redirect: groups_path
+
+ expect(response).to redirect_to(groups_path)
+ end
+
+ it 'redirects to the referer when no redirect specified' do
+ request.env["HTTP_REFERER"] = groups_url
+
+ post :accept, id: term.id
+
+ expect(response).to redirect_to(groups_path)
+ end
+
+ context 'redirecting to another domain' do
+ it 'is prevented when passing a redirect param' do
+ post :accept, id: term.id, redirect: '//example.com/random/path'
+
+ expect(response).to redirect_to(root_path)
+ end
+
+ it 'is prevented when redirecting to the referer' do
+ request.env["HTTP_REFERER"] = 'http://example.com/and/a/path'
+
+ post :accept, id: term.id
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
+
+ describe 'POST #decline' do
+ it 'stores that the user declined the terms' do
+ post :decline, id: term.id
+
+ agreement = user.term_agreements.find_by(term: term)
+
+ expect(agreement.accepted).to eq(false)
+ end
+
+ it 'signs out the user' do
+ post :decline, id: term.id
+
+ expect(response).to redirect_to(root_path)
+ expect(assigns(:current_user)).to be_nil
+ end
+ end
+end
diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb
index c8d016070f5..db19e98b851 100644
--- a/spec/db/production/settings_spec.rb
+++ b/spec/db/production/settings_spec.rb
@@ -48,15 +48,15 @@ describe 'seed production settings' do
end
end
- context 'GITLAB_PROMETHEUS_METRICS_ENABLED is false' do
+ context 'GITLAB_PROMETHEUS_METRICS_ENABLED is default' do
before do
stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', '')
end
- it 'prometheus_metrics_enabled is set to false' do
+ it 'prometheus_metrics_enabled is set to true' do
load(settings_file)
- expect(settings.prometheus_metrics_enabled).to eq(false)
+ expect(settings.prometheus_metrics_enabled).to eq(true)
end
end
end
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
new file mode 100644
index 00000000000..c0b9a25bfe8
--- /dev/null
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :ci_build_trace_chunk, class: Ci::BuildTraceChunk do
+ build factory: :ci_build
+ chunk_index 0
+ data_store :redis
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 33de637ff7d..4acc008ed38 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -243,5 +243,10 @@ FactoryBot.define do
failed
failure_reason 1
end
+
+ trait :api_failure do
+ failed
+ failure_reason 2
+ end
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 34b8b246d0f..cdc170b9ccb 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -6,6 +6,7 @@ FactoryBot.define do
is_shared false
active true
access_level :not_protected
+ runner_type :project_type
trait :online do
contacted_at Time.now
@@ -13,6 +14,7 @@ FactoryBot.define do
trait :shared do
is_shared true
+ runner_type :instance_type
end
trait :specific do
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index 25309033571..ce61e6bf759 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -21,6 +21,7 @@ FactoryBot.define do
pipeline factory: :ci_empty_pipeline
name 'test'
+ position 1
status 'pending'
end
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 98566f907f9..0430762c1ff 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -4,8 +4,8 @@ FactoryBot.define do
name 'test-cluster'
trait :project do
- after(:create) do |cluster, evaluator|
- cluster.projects << create(:project)
+ before(:create) do |cluster, evaluator|
+ cluster.projects << create(:project, :repository)
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index ce5fbc343ee..53368c64e10 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -2,6 +2,7 @@ FactoryBot.define do
factory :commit_status, class: CommitStatus do
name 'default'
stage 'test'
+ stage_idx 0
status 'success'
description 'commit status'
pipeline factory: :ci_pipeline_with_one_job
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index d5d819d862a..818f7b046f6 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -1,4 +1,4 @@
-require_relative '../support/repo_helpers'
+require_relative '../support/helpers/repo_helpers'
FactoryBot.define do
factory :commit do
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
index 5fea4a9d5a6..017e866e69c 100644
--- a/spec/factories/deploy_tokens.rb
+++ b/spec/factories/deploy_tokens.rb
@@ -10,5 +10,13 @@ FactoryBot.define do
trait :revoked do
revoked true
end
+
+ trait :gitlab_deploy_token do
+ name DeployToken::GITLAB_DEPLOY_TOKEN_NAME
+ end
+
+ trait :expired do
+ expires_at { Date.today - 1.month }
+ end
end
end
diff --git a/spec/factories/gitaly/tag.rb b/spec/factories/gitaly/tag.rb
new file mode 100644
index 00000000000..e99776d524a
--- /dev/null
+++ b/spec/factories/gitaly/tag.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :gitaly_tag, class: Gitaly::Tag do
+ skip_create
+
+ name { 'v3.1.4' }
+ message { 'Pie release' }
+ target_commit factory: :gitaly_commit
+ end
+end
diff --git a/spec/factories/gpg_key_subkeys.rb b/spec/factories/gpg_key_subkeys.rb
index 57eaaee345f..6c7db5379a9 100644
--- a/spec/factories/gpg_key_subkeys.rb
+++ b/spec/factories/gpg_key_subkeys.rb
@@ -1,5 +1,3 @@
-require_relative '../support/gpg_helpers'
-
FactoryBot.define do
factory :gpg_key_subkey do
gpg_key
diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb
index b8aabf74221..51b8ddc9934 100644
--- a/spec/factories/gpg_keys.rb
+++ b/spec/factories/gpg_keys.rb
@@ -1,4 +1,4 @@
-require_relative '../support/gpg_helpers'
+require_relative '../support/helpers/gpg_helpers'
FactoryBot.define do
factory :gpg_key do
diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb
index 4620caff823..b89e6ffc4b3 100644
--- a/spec/factories/gpg_signature.rb
+++ b/spec/factories/gpg_signature.rb
@@ -1,5 +1,3 @@
-require_relative '../support/gpg_helpers'
-
FactoryBot.define do
factory :gpg_signature do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 8c531cf5909..3b354c0d96b 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -5,6 +5,14 @@ FactoryBot.define do
type 'Group'
owner nil
+ after(:create) do |group|
+ if group.owner
+ # We could remove this after we have proper constraint:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/43292
+ raise "Don't set owner for groups, use `group.add_owner(user)` instead"
+ end
+ end
+
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
end
diff --git a/spec/factories/import_state.rb b/spec/factories/import_state.rb
new file mode 100644
index 00000000000..15d0a9d466a
--- /dev/null
+++ b/spec/factories/import_state.rb
@@ -0,0 +1,38 @@
+FactoryBot.define do
+ factory :import_state, class: ProjectImportState do
+ status :none
+ association :project, factory: :project
+
+ transient do
+ import_url { generate(:url) }
+ end
+
+ trait :repository do
+ association :project, factory: [:project, :repository]
+ end
+
+ trait :none do
+ status :none
+ end
+
+ trait :scheduled do
+ status :scheduled
+ end
+
+ trait :started do
+ status :started
+ end
+
+ trait :finished do
+ status :finished
+ end
+
+ trait :failed do
+ status :failed
+ end
+
+ after(:create) do |import_state, evaluator|
+ import_state.project.update_columns(import_url: evaluator.import_url)
+ end
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index d26cb0c3417..fab0ec22450 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -41,6 +41,11 @@ FactoryBot.define do
state :merged
end
+ trait :merged_target do
+ source_branch "merged-target"
+ target_branch "improve/awesome"
+ end
+
trait :closed do
state :closed
end
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb
index f94b09cff15..6feafa5ece9 100644
--- a/spec/factories/namespaces.rb
+++ b/spec/factories/namespaces.rb
@@ -2,6 +2,22 @@ FactoryBot.define do
factory :namespace do
sequence(:name) { |n| "namespace#{n}" }
path { name.downcase.gsub(/\s/, '_') }
- owner
+
+ # This is a workaround to avoid the user creating another namespace via
+ # User#ensure_namespace_correct. We should try to remove it and then
+ # we could remove this workaround
+ association :owner, factory: :user, strategy: :build
+ before(:create) do |namespace|
+ owner = namespace.owner
+
+ if owner
+ # We're changing the username here because we want to keep our path,
+ # and User#ensure_namespace_correct would change the path based on
+ # username, so we're forced to do this otherwise we'll need to change
+ # a lot of existing tests.
+ owner.username = namespace.path
+ owner.namespace = namespace
+ end
+ end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 857333f222d..40f3fa7d69b 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -1,4 +1,4 @@
-require_relative '../support/repo_helpers'
+require_relative '../support/helpers/repo_helpers'
include ActionDispatch::TestProcess
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
index db2eb4fc863..4d21ed47f39 100644
--- a/spec/factories/project_wikis.rb
+++ b/spec/factories/project_wikis.rb
@@ -2,7 +2,7 @@ FactoryBot.define do
factory :project_wiki do
skip_create
- project
+ association :project, :wiki_repo
user { project.creator }
initialize_with { new(project, user) }
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1761b6e2a3b..16e025618a6 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -1,4 +1,4 @@
-require_relative '../support/test_env'
+require_relative '../support/helpers/test_env'
FactoryBot.define do
# Project without repository
@@ -15,14 +15,18 @@ FactoryBot.define do
namespace
creator { group ? create(:user) : namespace&.owner }
- # Nest Project Feature attributes
transient do
+ # Nest Project Feature attributes
wiki_access_level ProjectFeature::ENABLED
builds_access_level ProjectFeature::ENABLED
snippets_access_level ProjectFeature::ENABLED
issues_access_level ProjectFeature::ENABLED
merge_requests_access_level ProjectFeature::ENABLED
repository_access_level ProjectFeature::ENABLED
+
+ # we can't assign the delegated `#ci_cd_settings` attributes directly, as the
+ # `#ci_cd_settings` relation needs to be created first
+ group_runners_enabled nil
end
after(:create) do |project, evaluator|
@@ -47,6 +51,9 @@ FactoryBot.define do
end
project.group&.refresh_members_authorized_projects
+
+ # assign the delegated `#ci_cd_settings` attributes after create
+ project.reload.group_runners_enabled = evaluator.group_runners_enabled unless evaluator.group_runners_enabled.nil?
end
trait :public do
@@ -147,13 +154,39 @@ FactoryBot.define do
# We delete hooks so that gitlab-shell will not try to authenticate with
# an API that isn't running
- FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.disk_path}.git", 'hooks'))
+ project.gitlab_shell.rm_directory(project.repository_storage,
+ File.join("#{project.disk_path}.git", 'hooks'))
+ end
+ end
+
+ trait :remote_mirror do
+ transient do
+ remote_name "remote_mirror_#{SecureRandom.hex}"
+ url "http://foo.com"
+ enabled true
+ end
+ after(:create) do |project, evaluator|
+ project.remote_mirrors.create!(url: evaluator.url, enabled: evaluator.enabled)
+ end
+ end
+
+ trait :stubbed_repository do
+ after(:build) do |project|
+ allow(project).to receive(:empty_repo?).and_return(false)
+ allow(project.repository).to receive(:empty?).and_return(false)
end
end
trait :wiki_repo do
after(:create) do |project|
raise 'Failed to create wiki repository!' unless project.create_wiki
+
+ # We delete hooks so that gitlab-shell will not try to authenticate with
+ # an API that isn't running
+ project.gitlab_shell.rm_directory(
+ project.repository_storage,
+ File.join("#{project.wiki.repository.disk_path}.git", "hooks")
+ )
end
end
@@ -165,7 +198,8 @@ FactoryBot.define do
after(:create) do |project|
raise "Failed to create repository!" unless project.create_repository
- FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.disk_path}.git", 'refs'))
+ project.gitlab_shell.rm_directory(project.repository_storage,
+ File.join("#{project.disk_path}.git", 'refs'))
end
end
diff --git a/spec/factories/remote_mirrors.rb b/spec/factories/remote_mirrors.rb
new file mode 100644
index 00000000000..adc7da27522
--- /dev/null
+++ b/spec/factories/remote_mirrors.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :remote_mirror, class: 'RemoteMirror' do
+ association :project, :repository
+ url "http://foo:bar@test.com"
+ end
+end
diff --git a/spec/factories/term_agreements.rb b/spec/factories/term_agreements.rb
new file mode 100644
index 00000000000..557599e663d
--- /dev/null
+++ b/spec/factories/term_agreements.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :term_agreement do
+ term
+ user
+ end
+end
diff --git a/spec/factories/terms.rb b/spec/factories/terms.rb
new file mode 100644
index 00000000000..5ffca365a5f
--- /dev/null
+++ b/spec/factories/terms.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :term, class: ApplicationSetting::Term do
+ terms "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+ end
+end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
new file mode 100644
index 00000000000..134eb25e4b1
--- /dev/null
+++ b/spec/fast_spec_helper.rb
@@ -0,0 +1,10 @@
+require 'bundler/setup'
+
+ENV['GITLAB_ENV'] = 'test'
+ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
+
+require_relative '../config/settings'
+require_relative 'support/rspec'
+require 'active_support/all'
+
+ActiveSupport::Dependencies.autoload_paths << 'lib'
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 766cd4b0090..d8fcdebfc6d 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -45,7 +45,7 @@ describe "Admin::AbuseReports", :js do
visit admin_abuse_reports_path
expect(page).to have_selector('.pagination')
- expect(page).to have_selector('.pagination .page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ expect(page).to have_selector('.pagination .js-pagination-page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
end
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 6d8350e99f1..328e8f25f89 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -34,7 +34,7 @@ describe "Admin::Projects" do
expect(page).to have_content(project.name)
expect(page).to have_content(archived_project.name)
- expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
+ expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived')
end
it 'renders only archived projects', :js do
diff --git a/spec/features/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb
index 380cd5d7703..2503fc9067d 100644
--- a/spec/features/admin/admin_requests_profiles_spec.rb
+++ b/spec/features/admin/admin_requests_profiles_spec.rb
@@ -33,12 +33,12 @@ describe 'Admin::RequestsProfilesController' do
visit admin_requests_profiles_path
- within('.panel', text: '/gitlab-org/gitlab-ce') do
+ within('.card', text: '/gitlab-org/gitlab-ce') do
expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile1)}']", text: time1.to_s(:long))
expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile2)}']", text: time2.to_s(:long))
end
- within('.panel', text: '/gitlab-com/infrastructure') do
+ within('.card', text: '/gitlab-com/infrastructure') do
expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile3)}']", text: time3.to_s(:long))
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 8de2e3d199b..c33014cbb31 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -59,6 +59,47 @@ describe "Admin Runners" do
expect(page).to have_text 'No runners found'
end
end
+
+ context 'group runner' do
+ let(:group) { create(:group) }
+ let!(:runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
+
+ it 'shows the label and does not show the project count' do
+ visit admin_runners_path
+
+ within "#runner_#{runner.id}" do
+ expect(page).to have_selector '.badge', text: 'group'
+ expect(page).to have_text 'n/a'
+ end
+ end
+ end
+
+ context 'shared runner' do
+ it 'shows the label and does not show the project count' do
+ runner = create :ci_runner, :shared
+
+ visit admin_runners_path
+
+ within "#runner_#{runner.id}" do
+ expect(page).to have_selector '.badge', text: 'shared'
+ expect(page).to have_text 'n/a'
+ end
+ end
+ end
+
+ context 'specific runner' do
+ it 'shows the label and the project count' do
+ project = create :project
+ runner = create :ci_runner, projects: [project]
+
+ visit admin_runners_path
+
+ within "#runner_#{runner.id}" do
+ expect(page).to have_selector '.badge', text: 'specific'
+ expect(page).to have_text '1'
+ end
+ end
+ end
end
describe "Runner show page" do
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 7853d2952ea..dc025d82937 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -2,10 +2,13 @@ require 'spec_helper'
feature 'Admin updates settings' do
include StubENV
+ include TermsHelper
+
+ let(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- sign_in(create(:admin))
+ sign_in(admin)
visit admin_application_settings_path
end
@@ -85,6 +88,22 @@ feature 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
+ scenario 'Terms of Service' do
+ # Already have the admin accept terms, so they don't need to accept in this spec.
+ _existing_terms = create(:term)
+ accept_terms(admin)
+
+ page.within('.as-terms') do
+ check 'Require all users to accept Terms of Service when they access GitLab.'
+ fill_in 'Terms of Service Agreement', with: 'Be nice!'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.enforce_terms).to be(true)
+ expect(Gitlab::CurrentSettings.terms).to eq 'Be nice!'
+ expect(page).to have_content 'Application settings saved successfully'
+ end
+
scenario 'Modify oauth providers' do
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty
@@ -133,7 +152,7 @@ feature 'Admin updates settings' do
scenario 'Change CI/CD settings' do
page.within('.as-ci-cd') do
- check 'Enabled Auto DevOps (Beta) for projects by default'
+ check 'Enabled Auto DevOps for projects by default'
fill_in 'Auto devops domain', with: 'domain.com'
click_button 'Save changes'
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 8f0a3611052..9e3221577c7 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -283,14 +283,14 @@ describe "Admin::Users" do
end
it "lists group projects" do
- within(:css, '.append-bottom-default + .panel') do
+ within(:css, '.append-bottom-default + .card') do
expect(page).to have_content 'Group projects'
- expect(page).to have_link group.name, admin_group_path(group)
+ expect(page).to have_link group.name, href: admin_group_path(group)
end
end
it 'allows navigation to the group details' do
- within(:css, '.append-bottom-default + .panel') do
+ within(:css, '.append-bottom-default + .card') do
click_link group.name
end
within(:css, 'h3.page-title') do
@@ -300,7 +300,7 @@ describe "Admin::Users" do
end
it 'shows the group access level' do
- within(:css, '.append-bottom-default + .panel') do
+ within(:css, '.append-bottom-default + .card') do
expect(page).to have_content 'Developer'
end
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index f1ac73ff819..90cf5a53787 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -19,7 +19,7 @@ feature 'Admin uses repository checks' do
expect(page).to have_content('Repository check was triggered')
end
- scenario 'to see a single failed repository check' do
+ scenario 'to see a single failed repository check', :js do
project = create(:project)
project.update_columns(
last_repository_check_failed: true,
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index 18901a954cc..7a14a441088 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -81,7 +81,7 @@ describe 'Issue Boards add issue modal', :js do
expect(page).to have_content('2')
end
- expect(page).to have_selector('.card', count: 2)
+ expect(page).to have_selector('.board-card', count: 2)
end
end
@@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal', :js do
page.within('.add-issues-modal') do
click_link 'Selected issues'
- expect(page).not_to have_selector('.card')
+ expect(page).not_to have_selector('.board-card')
end
end
@@ -122,7 +122,7 @@ describe 'Issue Boards add issue modal', :js do
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -133,7 +133,7 @@ describe 'Issue Boards add issue modal', :js do
wait_for_requests
- expect(page).not_to have_selector('.card')
+ expect(page).not_to have_selector('.board-card')
expect(page).not_to have_content("You haven't added any issues to your project yet")
end
end
@@ -142,7 +142,7 @@ describe 'Issue Boards add issue modal', :js do
context 'selecing issues' do
it 'selects single issue' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
page.within('.nav-links') do
expect(page).to have_content('Selected issues 1')
@@ -152,7 +152,7 @@ describe 'Issue Boards add issue modal', :js do
it 'changes button text' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue')
end
@@ -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('.card .card-number').each do |el|
+ all('.board-card .board-card-number').each do |el|
el.click
end
@@ -170,11 +170,11 @@ describe 'Issue Boards add issue modal', :js do
it 'shows only selected issues on selected tab' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_link 'Selected issues'
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -200,7 +200,7 @@ describe 'Issue Boards add issue modal', :js do
it 'selects all that arent already selected' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
expect(page).to have_selector('.is-active', count: 1)
@@ -212,11 +212,11 @@ describe 'Issue Boards add issue modal', :js do
it 'unselects from selected tab' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_link 'Selected issues'
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
expect(page).not_to have_selector('.is-active')
end
@@ -226,19 +226,19 @@ describe 'Issue Boards add issue modal', :js do
context 'adding issues' do
it 'adds to board' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_button 'Add 1 issue'
end
page.within(find('.board:nth-child(2)')) do
- expect(page).to have_selector('.card')
+ expect(page).to have_selector('.board-card')
end
end
it 'adds to second list' do
page.within('.add-issues-modal') do
- first('.card .card-number').click
+ first('.board-card .board-card-number').click
click_button planning.title
@@ -248,7 +248,7 @@ describe 'Issue Boards add issue modal', :js do
end
page.within(find('.board:nth-child(3)')) do
- expect(page).to have_selector('.card')
+ expect(page).to have_selector('.board-card')
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 52ff47d57f9..e414345ac23 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -92,9 +92,9 @@ describe 'Issue Boards', :js do
wait_for_requests
expect(page).to have_selector('.board', count: 4)
- expect(find('.board:nth-child(2)')).to have_selector('.card')
- expect(find('.board:nth-child(3)')).to have_selector('.card')
- expect(find('.board:nth-child(4)')).to have_selector('.card')
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card')
+ expect(find('.board:nth-child(3)')).to have_selector('.board-card')
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card')
end
it 'shows description tooltip on list title' do
@@ -120,9 +120,9 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 0)
+ expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 1)
end
it 'search list' do
@@ -131,9 +131,9 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0)
end
it 'allows user to delete board' do
@@ -171,21 +171,21 @@ describe 'Issue Boards', :js do
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('58')
- expect(page).to have_selector('.card', count: 20)
+ expect(page).to have_selector('.board-card', count: 20)
expect(page).to have_content('Showing 20 of 58 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
wait_for_requests
- expect(page).to have_selector('.card', count: 40)
+ expect(page).to have_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 58 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
wait_for_requests
- expect(page).to have_selector('.card', count: 58)
+ expect(page).to have_selector('.board-card', count: 58)
expect(page).to have_content('Showing all issues')
end
end
@@ -204,7 +204,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 2)
expect(find('.board:nth-child(2)')).not_to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 2)
expect(find('.board:nth-child(4)')).to have_content(issue9.title)
expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
end
@@ -242,7 +242,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
- expect(find('.board:nth-child(3)').all('.card').last).to have_content(development.title)
+ expect(find('.board:nth-child(3)').all('.board-card').last).to have_content(development.title)
end
it 'issue moves between lists' do
@@ -253,7 +253,7 @@ describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
- expect(find('.board:nth-child(2)').all('.card').first).to have_content(planning.title)
+ expect(find('.board:nth-child(2)').all('.board-card').first).to have_content(planning.title)
end
it 'issue moves from closed' do
@@ -335,7 +335,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(page).to have_css('#js-add-list.open')
+ expect(page).to have_css('#js-add-list.show')
end
it 'creates new list from a new label' do
@@ -425,12 +425,12 @@ describe 'Issue Boards', :js do
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('1')
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
page.within(find('.board:nth-child(3)')) do
expect(page.find('.board-header')).to have_content('0')
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
end
end
@@ -460,19 +460,19 @@ describe 'Issue Boards', :js do
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('51')
- expect(page).to have_selector('.card', count: 20)
+ expect(page).to have_selector('.board-card', count: 20)
expect(page).to have_content('Showing 20 of 51 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
- expect(page).to have_selector('.card', count: 40)
+ expect(page).to have_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 51 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
- expect(page).to have_selector('.card', count: 51)
+ expect(page).to have_selector('.board-card', count: 51)
expect(page).to have_content('Showing all issues')
end
end
@@ -494,8 +494,8 @@ describe 'Issue Boards', :js do
it 'filters by clicking label button on issue' do
page.within(find('.board:nth-child(2)')) do
- expect(page).to have_selector('.card', count: 8)
- expect(find('.card', match: :first)).to have_content(bug.title)
+ expect(page).to have_selector('.board-card', count: 8)
+ expect(find('.board-card', match: :first)).to have_content(bug.title)
click_button(bug.title)
wait_for_requests
end
@@ -512,13 +512,13 @@ describe 'Issue Boards', :js do
it 'removes label filter by clicking label button on issue' do
page.within(find('.board:nth-child(2)')) do
- page.within(find('.card', match: :first)) do
+ page.within(find('.board-card', match: :first)) do
click_button(bug.title)
end
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
wait_for_requests
@@ -589,7 +589,7 @@ describe 'Issue Boards', :js do
def wait_for_board_cards(board_number, expected_cards)
page.within(find(".board:nth-child(#{board_number})")) do
expect(page.find('.board-header')).to have_content(expected_cards.to_s)
- expect(page).to have_selector('.card', count: expected_cards)
+ expect(page).to have_selector('.board-card', count: expected_cards)
end
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 5abd02dbb48..193b1dfabbd 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -30,7 +30,7 @@ describe 'Issue Boards', :js do
it 'has un-ordered issue as last issue' do
page.within(find('.board:nth-child(2)')) do
- expect(all('.card').last).to have_content(issue4.title)
+ expect(all('.board-card').last).to have_content(issue4.title)
end
end
@@ -40,7 +40,7 @@ describe 'Issue Boards', :js do
wait_for_requests
page.within(find('.board:nth-child(2)')) do
- expect(first('.card')).to have_content(issue4.title)
+ expect(first('.board-card')).to have_content(issue4.title)
end
end
end
@@ -58,7 +58,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(first('.card')).to have_content(issue2.title)
+ expect(first('.board-card')).to have_content(issue2.title)
end
it 'moves from middle to bottom' do
@@ -66,7 +66,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(all('.card').last).to have_content(issue2.title)
+ expect(all('.board-card').last).to have_content(issue2.title)
end
it 'moves from top to bottom' do
@@ -74,7 +74,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(all('.card').last).to have_content(issue3.title)
+ expect(all('.board-card').last).to have_content(issue3.title)
end
it 'moves from bottom to top' do
@@ -82,7 +82,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(first('.card')).to have_content(issue1.title)
+ expect(first('.board-card')).to have_content(issue1.title)
end
it 'moves from top to middle' do
@@ -90,7 +90,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(first('.card')).to have_content(issue2.title)
+ expect(first('.board-card')).to have_content(issue2.title)
end
it 'moves from bottom to middle' do
@@ -98,7 +98,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(all('.card').last).to have_content(issue2.title)
+ expect(all('.board-card').last).to have_content(issue2.title)
end
end
@@ -121,11 +121,11 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2)
- expect(all('.board')[2]).to have_selector('.card', count: 4)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2)
+ expect(all('.board')[2]).to have_selector('.board-card', count: 4)
page.within(all('.board')[2]) do
- expect(first('.card')).to have_content(issue3.title)
+ expect(first('.board-card')).to have_content(issue3.title)
end
end
@@ -134,11 +134,11 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2)
- expect(all('.board')[2]).to have_selector('.card', count: 4)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2)
+ expect(all('.board')[2]).to have_selector('.board-card', count: 4)
page.within(all('.board')[2]) do
- expect(all('.card').last).to have_content(issue3.title)
+ expect(all('.board-card').last).to have_content(issue3.title)
end
end
@@ -147,11 +147,11 @@ describe 'Issue Boards', :js do
wait_for_requests
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2)
- expect(all('.board')[2]).to have_selector('.card', count: 4)
+ expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 2)
+ expect(all('.board')[2]).to have_selector('.board-card', count: 4)
page.within(all('.board')[2]) do
- expect(all('.card')[1]).to have_content(issue3.title)
+ expect(all('.board-card')[1]).to have_content(issue3.title)
end
end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 5907bb0840f..be9c6a51c29 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -38,7 +38,7 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
click_button 'Cancel'
end
@@ -48,7 +48,7 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -62,13 +62,13 @@ describe 'Issue Boards add issue modal filtering', :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
find('.clear-search').click
wait_for_requests
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -90,7 +90,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.name)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
@@ -113,7 +113,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'none')
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -126,7 +126,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: user2.name)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
@@ -148,7 +148,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'upcoming')
- expect(page).to have_selector('.card', count: 0)
+ expect(page).to have_selector('.board-card', count: 0)
end
end
@@ -161,7 +161,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: milestone.name)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
@@ -183,7 +183,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: 'none')
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -196,7 +196,7 @@ describe 'Issue Boards add issue modal filtering', :js do
wait_for_requests
expect(page).to have_selector('.js-visual-token', text: label.title)
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 6769acb7c9c..7a95f5cf871 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -63,6 +63,13 @@ describe 'Issue Boards new issue', :js do
page.within(first('.board .issue-count-badge-count')) do
expect(page).to have_content('1')
end
+
+ page.within(first('.board-card')) do
+ issue = project.issues.find_by_title('bug')
+
+ expect(page).to have_content(issue.to_reference)
+ expect(page).to have_link(issue.title, href: issue_path(issue))
+ end
end
it 'shows sidebar when creating new issue' do
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index d4c44c1adf9..a03aa681827 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,7 +15,7 @@ describe 'Issue Boards', :js do
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
- let(:card) { find('.board:nth-child(2)').first('.card') }
+ let(:card) { find('.board:nth-child(2)').first('.board-card') }
around do |example|
Timecop.freeze { example.run }
@@ -75,7 +75,7 @@ describe 'Issue Boards', :js do
wait_for_requests
page.within(find('.board:nth-child(2)')) do
- expect(page).to have_selector('.card', count: 1)
+ expect(page).to have_selector('.board-card', count: 1)
end
end
@@ -86,11 +86,11 @@ describe 'Issue Boards', :js do
visit project_board_path(project, board)
wait_for_requests
- click_card(find('.board:nth-child(1)').first('.card'))
+ click_card(find('.board:nth-child(1)').first('.board-card'))
expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
- click_card(find('.board:nth-child(3)').first('.card'))
+ click_card(find('.board:nth-child(3)').first('.board-card'))
expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board'
end
@@ -117,7 +117,7 @@ describe 'Issue Boards', :js do
end
it 'removes the assignee' do
- card_two = find('.board:nth-child(2)').find('.card:nth-child(2)')
+ card_two = find('.board:nth-child(2)').find('.board-card:nth-child(2)')
click_card(card_two)
page.within('.assignee') do
@@ -171,7 +171,7 @@ describe 'Issue Boards', :js do
end
page.within(find('.board:nth-child(2)')) do
- find('.card:nth-child(2)').click
+ find('.board-card:nth-child(2)').click
end
page.within('.assignee') do
@@ -237,6 +237,22 @@ describe 'Issue Boards', :js do
end
context 'labels' do
+ it 'shows current labels when editing' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_requests
+
+ page.within('.value') do
+ expect(page).to have_selector('.badge', count: 2)
+ expect(page).to have_content(development.title)
+ expect(page).to have_content(stretch.title)
+ end
+ end
+ end
+
it 'adds a single label' do
click_card(card)
@@ -252,12 +268,12 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 3)
+ expect(page).to have_selector('.badge', count: 3)
expect(page).to have_content(bug.title)
end
end
- expect(card).to have_selector('.label', count: 3)
+ expect(card).to have_selector('.badge', count: 3)
expect(card).to have_content(bug.title)
end
@@ -277,13 +293,13 @@ describe 'Issue Boards', :js do
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 4)
+ expect(page).to have_selector('.badge', count: 4)
expect(page).to have_content(bug.title)
expect(page).to have_content(regression.title)
end
end
- expect(card).to have_selector('.label', count: 4)
+ expect(card).to have_selector('.badge', count: 4)
expect(card).to have_content(bug.title)
expect(card).to have_content(regression.title)
end
@@ -296,19 +312,21 @@ describe 'Issue Boards', :js do
wait_for_requests
- click_link stretch.title
+ within('.dropdown-menu-labels') do
+ click_link stretch.title
+ end
wait_for_requests
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_selector('.badge', count: 1)
expect(page).not_to have_content(stretch.title)
end
end
- expect(card).to have_selector('.label', count: 1)
+ expect(card).to have_selector('.badge', count: 1)
expect(card).not_to have_content(stretch.title)
end
diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb
index 5fdb8044db2..271c610dcc8 100644
--- a/spec/features/boards/sub_group_project_spec.rb
+++ b/spec/features/boards/sub_group_project_spec.rb
@@ -20,7 +20,7 @@ describe 'Sub-group project issue boards', :js do
end
it 'creates new label from sidebar' do
- find('.card').click
+ find('.board-card').click
page.within '.labels' do
click_link 'Edit'
diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb
index a71020002dc..ed47f7ed390 100644
--- a/spec/features/dashboard/groups_list_spec.rb
+++ b/spec/features/dashboard/groups_list_spec.rb
@@ -40,7 +40,7 @@ feature 'Dashboard Groups page', :js do
expect(page).to have_content(nested_group.name)
end
- describe 'when filtering groups', :nested_groups do
+ context 'when filtering groups', :nested_groups do
before do
group.add_owner(user)
nested_group.add_owner(user)
@@ -75,7 +75,7 @@ feature 'Dashboard Groups page', :js do
end
end
- describe 'group with subgroups', :nested_groups do
+ context 'with subgroups', :nested_groups do
let!(:subgroup) { create(:group, :public, parent: group) }
before do
@@ -106,7 +106,7 @@ feature 'Dashboard Groups page', :js do
end
end
- describe 'when using pagination' do
+ context 'when using pagination' do
let(:group) { create(:group, created_at: 5.days.ago) }
let(:group2) { create(:group, created_at: 2.days.ago) }
@@ -141,4 +141,20 @@ feature 'Dashboard Groups page', :js do
expect(page).not_to have_selector("#group-#{group2.id}")
end
end
+
+ context 'when signed in as admin' do
+ let(:admin) { create(:admin) }
+
+ it 'shows only groups admin is member of' do
+ group.add_owner(admin)
+ expect(another_group).to be_persisted
+
+ sign_in(admin)
+ visit dashboard_groups_path
+ wait_for_requests
+
+ expect(page).to have_content(group.name)
+ expect(page).not_to have_content(another_group.name)
+ end
+ end
end
diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb
index c965b565ca3..8cd57f4f327 100644
--- a/spec/features/dashboard/milestone_filter_spec.rb
+++ b/spec/features/dashboard/milestone_filter_spec.rb
@@ -10,13 +10,16 @@ feature 'Dashboard > milestone filter', :js do
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)
- visit issues_dashboard_path(author_id: user.id)
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
@@ -24,31 +27,51 @@ feature 'Dashboard > milestone filter', :js do
end
context 'filtering by milestone' do
- milestone_select_selector = '.js-milestone-select'
-
before do
- filter_item_select('v1.0', milestone_select_selector)
- find(milestone_select_selector).click
+ 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('.dropdown-content')).to have_selector('a.is-active', 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
- expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+ page.within '.milestone-filter' do
+ expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1)
+
+ find('.dropdown-menu-close').click
- # open & close dropdown
- 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
- expect(find('.milestone-filter')).not_to have_selector('.dropdown.open')
+ it 'removes milestone filter from URL after clicking "Any Milestone"' do
+ expect(current_url).to include("milestone_title=#{milestone.title}")
- find(milestone_select_selector).click
+ find('.milestone-filter .dropdown-content li', text: 'Any Milestone').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')
+ expect(current_url).not_to include('milestone_title')
end
end
end
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 94133c62b5c..5ed20b02a6e 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -246,7 +246,7 @@ feature 'Dashboard Todos' do
it 'is has the right number of pages' do
visit dashboard_todos_path
- expect(page).to have_selector('.gl-pagination .page', count: 2)
+ expect(page).to have_selector('.gl-pagination .js-pagination-page', count: 2)
end
describe 'mark all as done', :js do
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index 8d5233d0c0f..2516db5422f 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -66,7 +66,7 @@ feature 'Top Plus Menu', :js do
page.within '.header-content' do
find('.header-new-dropdown-toggle').click
- expect(page).to have_selector('.header-new.dropdown.open', count: 1)
+ expect(page).to have_selector('.header-new.dropdown.show', count: 1)
find('.header-new-project-snippet a').click
end
@@ -88,7 +88,7 @@ feature 'Top Plus Menu', :js do
page.within '.header-content' do
find('.header-new-dropdown-toggle').click
- expect(page).to have_selector('.header-new.dropdown.open', count: 1)
+ expect(page).to have_selector('.header-new.dropdown.show', count: 1)
find('.header-new-group-project a').click
end
@@ -156,7 +156,7 @@ feature 'Top Plus Menu', :js do
def click_topmenuitem(item_name)
page.within '.header-content' do
find('.header-new-dropdown-toggle').click
- expect(page).to have_selector('.header-new.dropdown.open', count: 1)
+ expect(page).to have_selector('.header-new.dropdown.show', count: 1)
click_link item_name
end
end
diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb
new file mode 100644
index 00000000000..5ddb5894624
--- /dev/null
+++ b/spec/features/groups/members/filter_members_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Filter members' do
+ let(:user) { create(:user) }
+ let(:user_with_2fa) { create(:user, :two_factor_via_otp) }
+ let(:group) { create(:group) }
+
+ background do
+ group.add_owner(user)
+ group.add_master(user_with_2fa)
+
+ sign_in(user)
+ end
+
+ scenario 'shows all members' do
+ visit_members_list
+
+ expect(first_member).to include(user.name)
+ expect(second_member).to include(user_with_2fa.name)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Everyone')
+ end
+
+ scenario 'shows only 2FA members' do
+ visit_members_list(two_factor: 'enabled')
+
+ expect(first_member).to include(user_with_2fa.name)
+ expect(members_list.size).to eq(1)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Enabled')
+ end
+
+ scenario 'shows only non 2FA members' do
+ visit_members_list(two_factor: 'disabled')
+
+ expect(first_member).to include(user.name)
+ expect(members_list.size).to eq(1)
+ expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Disabled')
+ end
+
+ def visit_members_list(options = {})
+ visit group_group_members_path(group.to_param, options)
+ end
+
+ def members_list
+ page.all('ul.content-list > li')
+ end
+
+ def first_member
+ members_list.first.text
+ end
+
+ def second_member
+ members_list.last.text
+ end
+end
diff --git a/spec/features/groups/members/manage_access_requests_spec.rb b/spec/features/groups/members/manage_access_requests_spec.rb
deleted file mode 100644
index b83cd657ef7..00000000000
--- a/spec/features/groups/members/manage_access_requests_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-feature 'Groups > Members > Manage access requests' do
- let(:user) { create(:user) }
- let(:owner) { create(:user) }
- let(:group) { create(:group, :public, :access_requestable) }
-
- background do
- group.request_access(user)
- group.add_owner(owner)
- sign_in(owner)
- end
-
- scenario 'owner can see access requests' do
- visit group_group_members_path(group)
-
- expect_visible_access_request(group, user)
- end
-
- scenario 'owner can grant access' do
- visit group_group_members_path(group)
-
- expect_visible_access_request(group, user)
-
- perform_enqueued_jobs { click_on 'Grant access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted"
- end
-
- scenario 'owner can deny access' do
- visit group_group_members_path(group)
-
- expect_visible_access_request(group, user)
-
- perform_enqueued_jobs { click_on 'Deny access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was denied"
- end
-
- def expect_visible_access_request(group, user)
- expect(group.requesters.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "Users requesting access to #{group.name} 1"
- expect(page).to have_content user.name
- end
-end
diff --git a/spec/features/groups/members/master_manages_access_requests_spec.rb b/spec/features/groups/members/master_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..2fd6d1ec599
--- /dev/null
+++ b/spec/features/groups/members/master_manages_access_requests_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Master manages access requests' do
+ it_behaves_like 'Master manages access requests' do
+ let(:entity) { create(:group, :public, :access_requestable) }
+ let(:members_page_path) { group_group_members_path(entity) }
+ end
+end
diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb
index 31fbbcf562c..e7efdf7dfef 100644
--- a/spec/features/groups/members/search_members_spec.rb
+++ b/spec/features/groups/members/search_members_spec.rb
@@ -22,7 +22,7 @@ describe 'Search group member' do
find('.member-search-btn').click
end
- group_members_list = find(".panel .content-list")
+ group_members_list = find(".card .content-list")
expect(group_members_list).to have_content(member.name)
expect(group_members_list).not_to have_content(user.name)
end
diff --git a/spec/features/groups/settings/group_badges_spec.rb b/spec/features/groups/settings/group_badges_spec.rb
index 92217294446..a99da4a119b 100644
--- a/spec/features/groups/settings/group_badges_spec.rb
+++ b/spec/features/groups/settings/group_badges_spec.rb
@@ -21,7 +21,7 @@ feature 'Group Badges' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[0]).to have_content badge_1.link_url
expect(rows[1]).to have_content badge_2.link_url
@@ -48,7 +48,7 @@ feature 'Group Badges' do
click_button 'Add badge'
wait_for_requests
- within '.panel-body' do
+ within '.card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@@ -60,7 +60,7 @@ feature 'Group Badges' do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
@@ -74,7 +74,7 @@ feature 'Group Badges' do
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
within 'form' do
@@ -85,7 +85,7 @@ feature 'Group Badges' do
wait_for_requests
end
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@@ -99,7 +99,7 @@ feature 'Group Badges' do
it 'shows a modal when deleting a badge' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@@ -109,14 +109,14 @@ feature 'Group Badges' do
it 'deletes a badge when confirming the modal' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 1
expect(rows[0]).to have_content badge_1.link_url
end
diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb
index e81c3180e78..916363c41dd 100644
--- a/spec/features/groups/user_browse_projects_group_page_spec.rb
+++ b/spec/features/groups/user_browse_projects_group_page_spec.rb
@@ -21,7 +21,7 @@ describe 'User browse group projects page' do
visit projects_group_path(group)
expect(page).to have_link project.name
- expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
+ expect(page).to have_xpath("//span[@class='badge badge-warning']", text: 'archived')
end
end
end
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index e4be6193b8b..a986ddc4abc 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -5,18 +5,41 @@ describe 'Invites' do
let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) }
- let(:invite) { group.group_members.invite.last }
+ let(:group_invite) { group.group_members.invite.last }
before do
project.add_master(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
- invite.generate_invite_token!
+ group_invite.generate_invite_token!
+ end
+
+ def confirm_email_and_sign_in(new_user)
+ new_user_token = User.find_by_email(new_user.email).confirmation_token
+
+ visit user_confirmation_path(confirmation_token: new_user_token)
+ fill_in_sign_in_form(new_user)
+ end
+
+ def fill_in_sign_up_form(new_user)
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ fill_in 'new_user_password', with: new_user.password
+ click_button "Register"
+ end
+
+ def fill_in_sign_in_form(user)
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: user.password
+ check 'user_remember_me'
+ click_button 'Sign in'
end
context 'when signed out' do
before do
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'renders sign in page with sign in notice' do
@@ -25,12 +48,9 @@ describe 'Invites' do
end
it 'sign in and redirects to invitation page' do
- fill_in 'user_login', with: user.email
- fill_in 'user_password', with: user.password
- check 'user_remember_me'
- click_button 'Sign in'
+ fill_in_sign_in_form(user)
- expect(current_path).to eq(invite_path(invite.raw_invite_token))
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content(
'You have been invited by John Doe to join group Owned as Developer.'
)
@@ -45,7 +65,7 @@ describe 'Invites' do
end
it 'shows message user already a member' do
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
expect(page).to have_content('However, you are already a member of this group.')
end
end
@@ -53,7 +73,7 @@ describe 'Invites' do
describe 'accepting the invitation' do
before do
sign_in(user)
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
@@ -69,7 +89,7 @@ describe 'Invites' do
context 'when signed in' do
before do
sign_in(user)
- visit invite_path(invite.raw_invite_token)
+ visit invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
@@ -83,7 +103,7 @@ describe 'Invites' do
context 'when signed out' do
before do
- visit decline_invite_path(invite.raw_invite_token)
+ visit decline_invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
@@ -94,4 +114,72 @@ describe 'Invites' do
end
end
end
+
+ describe 'invite an user using their email address' do
+ let(:new_user) { build_stubbed(:user) }
+ let(:invite_email) { new_user.email }
+ let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) }
+ let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) }
+
+ before do
+ stub_application_setting(send_user_confirmation_email: send_email_confirmation)
+ visit invite_path(group_invite.raw_invite_token)
+ end
+
+ context 'email confirmation disabled' do
+ let(:send_email_confirmation) { false }
+
+ it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
+ fill_in_sign_up_form(new_user)
+
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(page).to have_content(project.full_name)
+ visit group_path(group)
+ expect(page).to have_content(group.full_name)
+ end
+
+ context 'the user sign-up using a different email address' do
+ let(:invite_email) { build_stubbed(:user).email }
+
+ it 'signs up and redirects to the invitation page' do
+ fill_in_sign_up_form(new_user)
+
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
+ end
+ end
+ end
+
+ context 'email confirmation enabled' do
+ let(:send_email_confirmation) { true }
+
+ it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
+ fill_in_sign_up_form(new_user)
+ confirm_email_and_sign_in(new_user)
+
+ expect(current_path).to eq(root_path)
+ expect(page).to have_content(project.full_name)
+ visit group_path(group)
+ expect(page).to have_content(group.full_name)
+ end
+
+ it "doesn't accept invitations until the user confirm his email" do
+ fill_in_sign_up_form(new_user)
+ sign_in(owner)
+
+ visit project_project_members_path(project)
+ expect(page).to have_content 'Invited'
+ end
+
+ context 'the user sign-up using a different email address' do
+ let(:invite_email) { build_stubbed(:user).email }
+
+ it 'signs up and redirects to the invitation page' do
+ fill_in_sign_up_form(new_user)
+ confirm_email_and_sign_in(new_user)
+
+ expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
new file mode 100644
index 00000000000..9613e22bf24
--- /dev/null
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -0,0 +1,140 @@
+require "rails_helper"
+
+describe "Internal references", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+
+ let(:private_project_user) { private_project.owner }
+ let(:private_project) { create(:project, :private, :repository) }
+ let(:private_project_issue) { create(:issue, project: private_project) }
+ let(:private_project_merge_request) { create(:merge_request, source_project: private_project) }
+ let(:public_project_user) { public_project.owner }
+ let(:public_project) { create(:project, :public, :repository) }
+ let(:public_project_issue) { create(:issue, project: public_project) }
+ let(:public_project_merge_request) { create(:merge_request, source_project: public_project) }
+
+ context "when referencing to open issue" do
+ context "from private project" do
+ context "from issue" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_issue_path(private_project, private_project_issue))
+
+ add_note("##{public_project_issue.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_issue_path(public_project, public_project_issue))
+ end
+
+ it { expect(page).not_to have_css(".note") }
+ end
+ end
+
+ context "from merge request" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_merge_request_path(private_project, private_project_merge_request))
+
+ add_note("##{public_project_issue.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_issue_path(public_project, public_project_issue))
+ end
+
+ it "doesn't show any references" do
+ page.within(".issue-details") do
+ expect(page).not_to have_content("#merge-requests .merge-requests-title")
+ end
+ end
+ end
+
+ context "when user has access to private project" do
+ before do
+ visit(project_issue_path(public_project, public_project_issue))
+ end
+
+ it "shows references" do
+ page.within("#merge-requests .merge-requests-title") do
+ expect(page).to have_content("1 Related Merge Request")
+ end
+
+ page.within("#merge-requests ul") do
+ expect(page).to have_content(private_project_merge_request.title)
+ end
+
+ expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
+ .and have_content(private_project_user.name)
+ end
+ end
+ end
+ end
+ end
+
+ context "when referencing to open merge request" do
+ context "from private project" do
+ context "from issue" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_issue_path(private_project, private_project_issue))
+
+ add_note("##{public_project_merge_request.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it { expect(page).not_to have_css(".note") }
+ end
+ end
+
+ context "from merge request" do
+ before do
+ sign_in(private_project_user)
+
+ visit(project_merge_request_path(private_project, private_project_merge_request))
+
+ add_note("##{public_project_merge_request.to_reference(private_project)}")
+ end
+
+ context "when user doesn't have access to private project" do
+ before do
+ sign_in(public_project_user)
+
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it "doesn't show any references" do
+ page.within(".merge-request-details") do
+ expect(page).not_to have_content("#merge-requests .merge-requests-title")
+ end
+ end
+ end
+
+ context "when user has access to private project" do
+ before do
+ visit(project_merge_request_path(public_project, public_project_merge_request))
+ end
+
+ it "shows references" do
+ expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
+ .and have_content(private_project_user.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
new file mode 100644
index 00000000000..fa0ab88624e
--- /dev/null
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -0,0 +1,187 @@
+require "rails_helper"
+
+describe "Jira", :js do
+ let(:user) { create(:user) }
+ let(:actual_project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
+ let(:issue_actual_project) { create(:issue, project: actual_project) }
+ let!(:other_project) { create(:project, :public) }
+ let!(:issue_other_project) { create(:issue, project: other_project) }
+ let(:issues) { [issue_actual_project, issue_other_project] }
+
+ shared_examples "correct references" do
+ before do
+ remotelink = double(:remotelink, all: [], build: double(save!: true))
+
+ stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
+ stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:remotelink).and_return(remotelink)
+
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+
+ build_note
+ end
+
+ it "creates a link to the referenced issue on the preview" do
+ find(".js-md-preview-button").click
+
+ wait_for_requests
+
+ page.within(".md-preview-holder") do
+ links_expectations
+ end
+ end
+
+ it "creates a link to the referenced issue after submit" do
+ click_button("Comment")
+
+ wait_for_requests
+
+ page.within("#diff-notes-app") do
+ links_expectations
+ end
+ end
+
+ it "creates a note on the referenced issues" do
+ click_button("Comment")
+
+ wait_for_requests
+
+ if referenced_issues.include?(issue_actual_project)
+ visit(issue_path(issue_actual_project))
+
+ page.within("#notes") do
+ expect(page).to have_content("#{user.to_reference} mentioned in merge request #{merge_request.to_reference}")
+ end
+ end
+
+ if referenced_issues.include?(issue_other_project)
+ visit(issue_path(issue_other_project))
+
+ page.within("#notes") do
+ expect(page).to have_content("#{user.to_reference} mentioned in merge request #{merge_request.to_reference(other_project)}")
+ end
+ end
+ end
+ end
+
+ context "when internal issues tracker is enabled for the other project" do
+ context "when only internal issues tracker is enabled for the actual project" do
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project, issue_other_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+
+ context "when both external and internal issues trackers are enabled for the actual project" do
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project, issue_other_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when only external issues tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_other_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when no tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_other_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+ end
+
+ context "when internal issues tracker is disabled for the other project" do
+ let(:other_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ context "when only internal issues tracker is enabled for the actual project" do
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+
+ context "when both external and internal issues trackers are enabled for the actual project" do
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [issue_actual_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when only external issues tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context "when no issues tracker is enabled for the actual project" do
+ let(:actual_project) { create(:project, :public, :repository, :issues_disabled) }
+
+ include_examples "correct references" do
+ let(:referenced_issues) { [] }
+ let(:jira_referenced) { false }
+ end
+ end
+ end
+
+ private
+
+ def build_note
+ markdown = <<~HEREDOC
+ Referencing internal issue #{issue_actual_project.to_reference},
+ cross-project #{issue_other_project.to_reference(actual_project)} external JIRA-5
+ and non existing #999
+ HEREDOC
+
+ page.within("#diff-notes-app") do
+ fill_in("note_note", with: markdown)
+ end
+ end
+
+ def links_expectations
+ issues.each do |issue|
+ if referenced_issues.include?(issue)
+ expect(page).to have_link(issue.to_reference, href: issue_path(issue))
+ else
+ expect(page).not_to have_link(issue.to_reference, href: issue_path(issue))
+ end
+ end
+
+ if jira_referenced
+ expect(page).to have_link("JIRA-5", href: "https://jira.example.com/browse/JIRA-5")
+ else
+ expect(page).not_to have_link("JIRA-5", href: "https://jira.example.com/browse/JIRA-5")
+ end
+
+ expect(page).not_to have_link("#999")
+ end
+end
diff --git a/spec/features/issuables/markdown_references_spec.rb b/spec/features/issuables/markdown_references_spec.rb
deleted file mode 100644
index dd4e10a9886..00000000000
--- a/spec/features/issuables/markdown_references_spec.rb
+++ /dev/null
@@ -1,193 +0,0 @@
-require 'rails_helper'
-
-describe 'Markdown References', :js do
- let(:user) { create(:user) }
- let(:actual_project) { create(:project, :public, :repository) }
- let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project)}
- let(:issue_actual_project) { create(:issue, project: actual_project) }
- let!(:other_project) { create(:project, :public) }
- let!(:issue_other_project) { create(:issue, project: other_project) }
- let(:issues) { [issue_actual_project, issue_other_project] }
-
- def build_note
- markdown = "Referencing internal issue #{issue_actual_project.to_reference}, " +
- "cross-project #{issue_other_project.to_reference(actual_project)} external JIRA-5 " +
- "and non existing #999"
-
- page.within('#diff-notes-app') do
- fill_in 'note_note', with: markdown
- end
- end
-
- shared_examples 'correct references' do
- before do
- remotelink = double(:remotelink, all: [], build: double(save!: true))
-
- stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
- stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
- allow_any_instance_of(JIRA::Resource::Issue).to receive(:remotelink).and_return(remotelink)
-
- sign_in(user)
- visit merge_request_path(merge_request)
- build_note
- end
-
- def links_expectations
- issues.each do |issue|
- if referenced_issues.include?(issue)
- expect(page).to have_link(issue.to_reference, href: issue_path(issue))
- else
- expect(page).not_to have_link(issue.to_reference, href: issue_path(issue))
- end
- end
-
- if jira_referenced
- expect(page).to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
- else
- expect(page).not_to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
- end
-
- expect(page).not_to have_link('#999')
- end
-
- it 'creates a link to the referenced issue on the preview' do
- find('.js-md-preview-button').click
- wait_for_requests
-
- page.within('.md-preview-holder') do
- links_expectations
- end
- end
-
- it 'creates a link to the referenced issue after submit' do
- click_button 'Comment'
- wait_for_requests
-
- page.within('#diff-notes-app') do
- links_expectations
- end
- end
-
- it 'creates a note on the referenced issues' do
- click_button 'Comment'
- wait_for_requests
-
- if referenced_issues.include?(issue_actual_project)
- visit issue_path(issue_actual_project)
-
- page.within('#notes') do
- expect(page).to have_content(
- "#{user.to_reference} mentioned in merge request #{merge_request.to_reference}"
- )
- end
- end
-
- if referenced_issues.include?(issue_other_project)
- visit issue_path(issue_other_project)
-
- page.within('#notes') do
- expect(page).to have_content(
- "#{user.to_reference} mentioned in merge request #{merge_request.to_reference(other_project)}"
- )
- end
- end
- end
- end
-
- context 'when internal issues tracker is enabled for the other project' do
- context 'when only internal issues tracker is enabled for the actual project' do
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project, issue_other_project] }
- let(:jira_referenced) { false }
- end
- end
-
- context 'when both external and internal issues trackers are enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project, issue_other_project] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when only external issues tracker is enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
-
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_other_project] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when no tracker is enabled for the actual project' do
- before do
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_other_project] }
- let(:jira_referenced) { false }
- end
- end
- end
-
- context 'when internal issues tracker is disabled for the other project' do
- before do
- other_project.issues_enabled = false
- other_project.save!
- end
-
- context 'when only internal issues tracker is enabled for the actual project' do
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project] }
- let(:jira_referenced) { false }
- end
- end
-
- context 'when both external and internal issues trackers are enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [issue_actual_project] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when only external issues tracker is enabled for the actual project' do
- before do
- create(:jira_service, project: actual_project)
-
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [] }
- let(:jira_referenced) { true }
- end
- end
-
- context 'when no issues tracker is enabled for the actual project' do
- before do
- actual_project.issues_enabled = false
- actual_project.save!
- end
-
- include_examples 'correct references' do
- let(:referenced_issues) { [] }
- let(:jira_referenced) { false }
- end
- end
- end
-end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 08ba91a2682..483122ae463 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -248,7 +248,7 @@ describe 'Filter issues', :js do
context 'issue label clicked' do
it 'filters and displays in search bar' do
- find('.issues-list .issue .issue-main-info .issuable-info a .label', text: multiple_words_label.title).click
+ find('.issues-list .issue .issue-main-info .issuable-info a .badge', text: multiple_words_label.title).click
expect_issues_list_count(1)
expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 27551bb70ee..830c794376d 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -5,9 +5,9 @@ feature 'Issue Sidebar' do
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
- let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
+ let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
let!(:xss_label) { create(:label, project: project, title: '&lt;script&gt;alert("xss");&lt;&#x2F;script&gt;') }
before do
@@ -112,11 +112,18 @@ feature 'Issue Sidebar' do
context 'editing issue labels', :js do
before do
+ issue.update_attributes(labels: [label])
page.within('.block.labels') do
find('.edit-link').click
end
end
+ it 'shows the current set of labels' do
+ page.within('.issuable-show-labels') do
+ expect(page).to have_content label.title
+ end
+ end
+
it 'shows option to create a project label' do
page.within('.block.labels') do
expect(page).to have_content 'Create project'
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 8e6493bbd93..4a44ec302fc 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do
it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
- expect(page).to have_content 'Mark done'
+ expect(page).to have_content 'Mark todo as done'
end
page.within '.header-content .todos-count' do
@@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do
it 'marks a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add todo'
- click_button 'Mark done'
+ click_button 'Mark todo as done'
end
expect(page).to have_selector('.todos-count', visible: false)
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 539d7e9ff01..3dfcbc2fcb8 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
@@ -139,8 +139,8 @@ describe 'User creates branch and merge request on issue page', :js do
end
it 'disables the create branch button' do
- expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hide)')
- expect(page).to have_css('.create-mr-dropdown-wrap .available.hide', visible: false)
+ expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)')
+ expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false)
expect(page).to have_content /1 Related Merge Request/
end
end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index ff2a0e15719..fd0aa6cf3a3 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -161,6 +161,7 @@ feature 'Issues > User uses quick actions', :js do
before do
target_project.add_master(user)
+ gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
end
@@ -178,9 +179,10 @@ feature 'Issues > User uses quick actions', :js do
end
context 'when the project is valid but the user not authorized' do
- let(:project_unauthorized) {create(:project, :public)}
+ let(:project_unauthorized) { create(:project, :public) }
before do
+ gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
end
@@ -195,6 +197,7 @@ feature 'Issues > User uses quick actions', :js do
context 'when the project is invalid' do
before do
+ gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
end
@@ -218,6 +221,7 @@ feature 'Issues > User uses quick actions', :js do
before do
target_project.add_master(user)
+ gitlab_sign_out
sign_in(user)
visit project_issue_path(project, issue)
end
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 3e05e7b7f38..4700ada1aae 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -34,7 +34,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
- expect(page).to have_selector('span.label', text: label.title)
+ expect(page).to have_selector('span.badge', text: label.title)
end
end
@@ -45,7 +45,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
- expect(page).not_to have_selector('span.label', text: child_group_label.title)
+ expect(page).not_to have_selector('span.badge', text: child_group_label.title)
end
end
@@ -57,7 +57,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
if board
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue.title)
end
else
@@ -96,11 +96,11 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
if board
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue.title)
end
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue_2.title)
end
else
@@ -118,11 +118,11 @@ feature 'Labels Hierarchy', :js, :nested_groups do
select_label_on_dropdown(group_label_3.title)
if board
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).not_to have_selector('a', text: labeled_issue_2.title)
end
- expect(page).to have_selector('.card-title') do |card|
+ expect(page).to have_selector('.board-card-title') do |card|
expect(card).to have_selector('a', text: labeled_issue_3.title)
end
else
@@ -159,9 +159,9 @@ feature 'Labels Hierarchy', :js, :nested_groups do
find('.btn-create').click
expect(page.find('.issue-details h2.title')).to have_content('new created issue')
- expect(page).to have_selector('span.label', text: grandparent_group_label.title)
- expect(page).to have_selector('span.label', text: parent_group_label.title)
- expect(page).to have_selector('span.label', text: project_label_1.title)
+ expect(page).to have_selector('span.badge', text: grandparent_group_label.title)
+ expect(page).to have_selector('span.badge', text: parent_group_label.title)
+ expect(page).to have_selector('span.badge', text: project_label_1.title)
end
end
@@ -170,6 +170,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
context 'on issue sidebar' do
before do
+ project_1.add_developer(user)
+
visit project_issue_path(project_1, issue)
end
@@ -180,11 +182,13 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, project: project_1) }
before do
+ project_1.add_developer(user)
+
visit project_board_path(project_1, board)
wait_for_requests
- find('.card').click
+ find('.board-card').click
end
it_behaves_like 'assigning labels from sidebar'
@@ -194,11 +198,13 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, group: parent) }
before do
+ parent.add_developer(user)
+
visit group_board_path(parent, board)
wait_for_requests
- find('.card').click
+ find('.board-card').click
end
it_behaves_like 'assigning labels from sidebar'
@@ -211,6 +217,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
context 'on project issuable list' do
before do
+ project_1.add_developer(user)
+
visit project_issues_path(project_1)
end
@@ -237,6 +245,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, project: project_1) }
before do
+ project_1.add_developer(user)
+
visit project_board_path(project_1, board)
end
@@ -247,6 +257,8 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, group: parent) }
before do
+ parent.add_developer(user)
+
visit group_board_path(parent, board)
end
@@ -259,6 +271,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, project: project_1) }
before do
+ project_1.add_developer(user)
visit project_board_path(project_1, board)
find('.js-new-board-list').click
wait_for_requests
@@ -281,6 +294,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
let(:board) { create(:board, group: parent) }
before do
+ parent.add_developer(user)
visit group_board_path(parent, board)
find('.js-new-board-list').click
wait_for_requests
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 19995559fae..59aa90fc86f 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -10,7 +10,7 @@ describe 'Merge request > User resolves conflicts', :js do
end
def create_merge_request(source_branch)
- create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr|
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project, merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
@@ -27,7 +27,7 @@ describe 'Merge request > User resolves conflicts', :js do
end
end
- find_button('Commit conflict resolution').send_keys(:return)
+ find_button('Commit to source branch').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -71,7 +71,7 @@ describe 'Merge request > User resolves conflicts', :js do
execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");')
end
- find_button('Commit conflict resolution').send_keys(:return)
+ find_button('Commit to source branch').send_keys(:return)
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
@@ -145,7 +145,7 @@ describe 'Merge request > User resolves conflicts', :js do
execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
end
- click_button 'Commit conflict resolution'
+ click_button 'Commit to source branch'
expect(page).to have_content('All merge conflicts were resolved')
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index b4ad4b64d8e..0fd2840c426 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -5,7 +5,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
let(:user) { project.creator }
let(:guest) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
- let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "| Markdown | Table |\n|-------|---------|\n| first | second |") }
let(:path) { "files/ruby/popen.rb" }
let(:position) do
Gitlab::Diff::Position.new(
@@ -111,6 +111,15 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
expect(page.find(".line-holder-placeholder")).to be_visible
expect(page.find(".timeline-content #note_#{note.id}")).to be_visible
end
+
+ it 'renders tables in lazy-loaded resolved diff dicussions' do
+ find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
+
+ wait_for_requests
+
+ expect(page.find(".timeline-content #note_#{note.id}")).not_to have_css(".line_holder")
+ expect(page.find(".timeline-content #note_#{note.id}")).to have_css("tr", count: 2)
+ end
end
describe 'side-by-side view' do
diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
new file mode 100644
index 00000000000..c40c720d168
--- /dev/null
+++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+
+describe 'Merge request > User sees Check out branch modal', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { project.creator }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ click_button('Check out branch')
+ end
+
+ it 'shows the check out branch modal' do
+ expect(page).to have_content('Check out, review, and merge locally')
+ end
+
+ it 'closes the check out branch model with Escape keypress' do
+ find('#modal_merge_info').send_keys(:escape)
+
+ expect(page).not_to have_content('Check out, review, and merge locally')
+ end
+end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index dbca279569a..ed6e29335d1 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -4,6 +4,12 @@ describe 'Merge request > User selects branches for new MR', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
+ def select_source_branch(branch_name)
+ find('.js-source-branch', match: :first).click
+ find('.js-source-branch-dropdown .dropdown-input-field').native.send_keys branch_name
+ find('.js-source-branch-dropdown .dropdown-content a', text: branch_name, match: :first).click
+ end
+
before do
project.add_master(user)
sign_in(user)
@@ -19,7 +25,7 @@ describe 'Merge request > User selects branches for new MR', :js do
expect(page).to have_content('Target branch')
first('.js-source-branch').click
- find('.dropdown-source-branch .dropdown-content a', match: :first).click
+ find('.js-source-branch-dropdown .dropdown-content a', match: :first).click
expect(page).to have_content "b83d6e3"
end
@@ -35,22 +41,15 @@ describe 'Merge request > User selects branches for new MR', :js do
expect(page).to have_content('Target branch')
first('.js-target-branch').click
- find('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0', match: :first).click
+ find('.js-target-branch-dropdown .dropdown-content a', text: 'v1.1.0', match: :first).click
expect(page).to have_content "b83d6e3"
end
it 'generates a diff for an orphaned branch' do
- visit project_merge_requests_path(project)
-
- page.within '.content' do
- click_link 'New merge request'
- end
- expect(page).to have_content('Source branch')
- expect(page).to have_content('Target branch')
+ visit project_new_merge_request_path(project)
- find('.js-source-branch', match: :first).click
- find('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch', match: :first).click
+ select_source_branch('orphaned-branch')
click_button "Compare branches"
click_link "Changes"
@@ -71,19 +70,18 @@ describe 'Merge request > User selects branches for new MR', :js do
first('.js-source-branch').click
- input = find('.dropdown-source-branch .dropdown-input-field')
- input.click
- input.send_keys('orphaned-branch')
+ page.within '.js-source-branch-dropdown' do
+ input = find('.dropdown-input-field')
+ input.click
+ input.send_keys('orphaned-branch')
- find('.dropdown-source-branch .dropdown-content li', match: :first)
- source_items = all('.dropdown-source-branch .dropdown-content li')
-
- expect(source_items.count).to eq(1)
+ expect(page).to have_css('.dropdown-content li', count: 1)
+ end
first('.js-target-branch').click
- find('.dropdown-target-branch .dropdown-content li', match: :first)
- target_items = all('.dropdown-target-branch .dropdown-content li')
+ find('.js-target-branch-dropdown .dropdown-content li', match: :first)
+ target_items = all('.js-target-branch-dropdown .dropdown-content li')
expect(target_items.count).to be > 1
end
@@ -171,10 +169,36 @@ describe 'Merge request > User selects branches for new MR', :js do
page.within('.merge-request') do
click_link 'Pipelines'
- wait_for_requests
expect(page).to have_content "##{pipeline.id}"
end
end
end
+
+ context 'with special characters in branch names' do
+ it 'escapes quotes in branch names' do
+ special_branch_name = '"with-quotes"'
+ CreateBranchService.new(project, user)
+ .execute(special_branch_name, 'add-pdf-file')
+
+ visit project_new_merge_request_path(project)
+ select_source_branch(special_branch_name)
+
+ source_branch_input = find('[name="merge_request[source_branch]"]', visible: false)
+ expect(source_branch_input.value).to eq special_branch_name
+ end
+
+ it 'does not escape unicode in branch names' do
+ special_branch_name = 'ʕ•ᴥ•ʔ'
+ CreateBranchService.new(project, user)
+ .execute(special_branch_name, 'add-pdf-file')
+
+ visit project_new_merge_request_path(project)
+ select_source_branch(special_branch_name)
+
+ click_button "Compare branches"
+
+ expect(page).to have_button("Submit merge request")
+ end
+ end
end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 6c51e4bbe26..b0db6870ddf 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -119,7 +119,7 @@ feature 'Milestone' do
find('.milestone-deprecation-message .js-popover-link').click
- expect(page).to have_selector('.milestone-deprecation-message .popover')
+ expect(page).to have_selector('.popover')
end
end
end
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index a5e325ee2e3..013cdaa6479 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -28,35 +28,46 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
OmniAuth.config.full_host = @omniauth_config_full_host
end
+ def login_with_provider(provider, enter_two_factor: false)
+ login_via(provider.to_s, user, uid, remember_me: remember_me)
+ enter_code(user.current_otp) if enter_two_factor
+ end
+
providers.each do |provider|
context "when the user logs in using the #{provider} provider" do
+ let(:uid) { 'my-uid' }
+ let(:remember_me) { false }
+ let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider.to_s) }
+ let(:two_factor_user) { create(:omniauth_user, :two_factor, extern_uid: uid, provider: provider.to_s) }
+
+ before do
+ stub_omniauth_config(provider)
+ end
+
context 'when two-factor authentication is disabled' do
it 'logs the user in' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid')
+ login_with_provider(provider)
expect(current_path).to eq root_path
end
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'logs the user in' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid')
+ login_with_provider(provider, enter_two_factor: true)
- enter_code(user.current_otp)
expect(current_path).to eq root_path
end
end
context 'when "remember me" is checked' do
+ let(:remember_me) { true }
+
context 'when two-factor authentication is disabled' do
it 'remembers the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: true)
+ login_with_provider(provider)
clear_browser_session
@@ -66,11 +77,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'remembers the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: true)
- enter_code(user.current_otp)
+ login_with_provider(provider, enter_two_factor: true)
clear_browser_session
@@ -83,9 +93,7 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
context 'when "remember me" is not checked' do
context 'when two-factor authentication is disabled' do
it 'does not remember the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: false)
+ login_with_provider(provider)
clear_browser_session
@@ -95,11 +103,10 @@ feature 'OAuth Login', :js, :allow_forgery_protection do
end
context 'when two-factor authentication is enabled' do
+ let(:user) { two_factor_user }
+
it 'does not remember the user after a browser restart' do
- stub_omniauth_config(provider)
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
- login_via(provider.to_s, user, 'my-uid', remember_me: false)
- enter_code(user.current_otp)
+ login_with_provider(provider, enter_two_factor: true)
clear_browser_session
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
new file mode 100644
index 00000000000..4045cfd21c4
--- /dev/null
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -0,0 +1,89 @@
+require 'rails_helper'
+
+feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
+ let(:user) do
+ create(:user).tap do |user|
+ user.current_sign_in_at = Time.current
+ end
+ end
+
+ around do |example|
+ Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
+ example.run
+ end
+ end
+
+ scenario 'User sees their active sessions' do
+ Capybara::Session.new(:session1)
+ Capybara::Session.new(:session2)
+
+ # note: headers can only be set on the non-js (aka. rack-test) driver
+ using_session :session1 do
+ Capybara.page.driver.header(
+ 'User-Agent',
+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0'
+ )
+
+ gitlab_sign_in(user)
+ end
+
+ # set an additional session on another device
+ using_session :session2 do
+ Capybara.page.driver.header(
+ 'User-Agent',
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]'
+ )
+
+ gitlab_sign_in(user)
+ end
+
+ using_session :session1 do
+ visit profile_active_sessions_path
+
+ expect(page).to have_content(
+ '127.0.0.1 ' \
+ 'This is your current session ' \
+ 'Firefox on Ubuntu ' \
+ 'Signed in on 12 Mar 09:06'
+ )
+
+ expect(page).to have_selector '[title="Desktop"]', count: 1
+
+ expect(page).to have_content(
+ '127.0.0.1 ' \
+ 'Last accessed on 12 Mar 09:06 ' \
+ 'Mobile Safari on iOS ' \
+ 'Signed in on 12 Mar 09:06'
+ )
+
+ expect(page).to have_selector '[title="Smartphone"]', count: 1
+ end
+ end
+
+ scenario 'User can revoke a session', :js, :redis_session_store do
+ Capybara::Session.new(:session1)
+ Capybara::Session.new(:session2)
+
+ # set an additional session in another browser
+ using_session :session2 do
+ gitlab_sign_in(user)
+ end
+
+ using_session :session1 do
+ gitlab_sign_in(user)
+ visit profile_active_sessions_path
+
+ expect(page).to have_link('Revoke', count: 1)
+
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).not_to have_link('Revoke')
+ end
+
+ using_session :session2 do
+ visit profile_active_sessions_path
+
+ expect(page).to have_content('You need to sign in or sign up before continuing.')
+ end
+ end
+end
diff --git a/spec/features/projects/actve_tabs_spec.rb b/spec/features/projects/actve_tabs_spec.rb
index 0bda68b83e7..ce5606b63ae 100644
--- a/spec/features/projects/actve_tabs_spec.rb
+++ b/spec/features/projects/actve_tabs_spec.rb
@@ -35,7 +35,7 @@ describe 'Project active tab' do
visit project_path(project)
end
- it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active tab', 'Project'
it_behaves_like 'page has active sub tab', 'Details'
context 'on project Home/Activity' do
@@ -43,7 +43,7 @@ describe 'Project active tab' do
click_tab('Activity')
end
- it_behaves_like 'page has active tab', 'Overview'
+ it_behaves_like 'page has active tab', 'Project'
it_behaves_like 'page has active sub tab', 'Activity'
end
end
diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb
deleted file mode 100644
index cb69aff8d5f..00000000000
--- a/spec/features/projects/artifacts/browse_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-feature 'Browse artifact', :js do
- let(:project) { create(:project, :public) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
- let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
- let(:browse_url) do
- browse_path('other_artifacts_0.1.2')
- end
-
- def browse_path(path)
- browse_project_job_artifacts_path(project, job, path)
- end
-
- context 'when visiting old URL' do
- before do
- visit browse_url.sub('/-/jobs', '/builds')
- end
-
- it "redirects to new URL" do
- expect(page.current_path).to eq(browse_url)
- end
- end
-
- context 'when browsing a directory with an text file' do
- let(:txt_entry) { job.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') }
-
- before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
- allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
- end
-
- context 'when the project is public' do
- it "shows external link icon and styles" do
- visit browse_url
-
- link = first('.tree-item-file-external-link')
-
- expect(page).to have_link('doc_sample.txt', href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path))
- expect(link[:target]).to eq('_blank')
- expect(link[:rel]).to include('noopener')
- expect(link[:rel]).to include('noreferrer')
- expect(page).to have_selector('.js-artifact-tree-external-icon')
- end
- end
-
- context 'when the project is private' do
- let!(:private_project) { create(:project, :private) }
- let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
- let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
- let(:user) { create(:user) }
-
- before do
- private_project.add_developer(user)
-
- sign_in(user)
- end
-
- it 'shows internal link styles' do
- visit browse_project_job_artifacts_path(private_project, job, 'other_artifacts_0.1.2')
-
- expect(page).to have_link('doc_sample.txt')
- expect(page).not_to have_selector('.js-artifact-tree-external-icon')
- end
- end
- end
-end
diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb
deleted file mode 100644
index 6f76c14910b..00000000000
--- a/spec/features/projects/artifacts/download_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'spec_helper'
-
-feature 'Download artifact' do
- let(:project) { create(:project, :public) }
- let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) }
- let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
-
- shared_examples 'downloading' do
- it 'downloads the zip' do
- expect(page.response_headers['Content-Disposition'])
- .to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"})
-
- # Check the content does match, but don't print this as error message
- expect(page.source.b == job.artifacts_file.file.read.b)
- end
- end
-
- context 'when downloading' do
- before do
- visit download_url
- end
-
- context 'via job id' do
- let(:download_url) do
- download_project_job_artifacts_path(project, job)
- end
-
- it_behaves_like 'downloading'
- end
-
- context 'via branch name and job name' do
- let(:download_url) do
- latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name)
- end
-
- it_behaves_like 'downloading'
- end
- end
-
- context 'when visiting old URL' do
- before do
- visit download_url.sub('/-/jobs', '/builds')
- end
-
- context 'via job id' do
- let(:download_url) do
- download_project_job_artifacts_path(project, job)
- end
-
- it_behaves_like 'downloading'
- end
-
- context 'via branch name and job name' do
- let(:download_url) do
- latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name)
- end
-
- it_behaves_like 'downloading'
- end
- end
-end
diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
new file mode 100644
index 00000000000..9ebbbaea911
--- /dev/null
+++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
@@ -0,0 +1,110 @@
+require "spec_helper"
+
+describe "User browses artifacts" do
+ let(:project) { create(:project, :public) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:browse_url) { browse_project_job_artifacts_path(project, job, "other_artifacts_0.1.2") }
+
+ context "when visiting old URL" do
+ it "redirects to new URL" do
+ visit(browse_url.sub("/-/jobs", "/builds"))
+
+ expect(page.current_path).to eq(browse_url)
+ end
+ end
+
+ context "when browsing artifacts root directory" do
+ before do
+ visit(browse_project_job_artifacts_path(project, job))
+ end
+
+ it "shows artifacts" do
+ expect(page).not_to have_selector(".build-sidebar")
+
+ page.within(".tree-table") do
+ expect(page).to have_no_content("..")
+ .and have_content("other_artifacts_0.1.2")
+ .and have_content("ci_artifacts.txt")
+ .and have_content("rails_sample.jpg")
+ end
+
+ page.within(".build-header") do
+ expect(page).to have_content("Job ##{job.id} in pipeline ##{pipeline.id} for #{pipeline.short_sha}")
+ end
+ end
+
+ it "shows an artifact" do
+ click_link("ci_artifacts.txt")
+
+ expect(page).to have_link("download it")
+ end
+ end
+
+ context "when browsing a directory with UTF-8 characters in its name" do
+ before do
+ visit(browse_project_job_artifacts_path(project, job))
+ end
+
+ it "shows correct content", :js do
+ page.within(".tree-table") do
+ click_link("tests_encoding")
+
+ expect(page).to have_no_content("non-utf8-dir")
+
+ click_link("utf8 test dir ✓")
+
+ expect(page).to have_content("..").and have_content("regular_file_2")
+ end
+ end
+ end
+
+ context "when browsing a directory with a text file" do
+ let(:txt_entry) { job.artifacts_metadata_entry("other_artifacts_0.1.2/doc_sample.txt") }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true)
+ end
+
+ context "when the project is public" do
+ before do
+ visit(browse_url)
+ end
+
+ it "shows correct content" do
+ link = first(".tree-item-file-external-link")
+
+ expect(link[:target]).to eq("_blank")
+ expect(link[:rel]).to include("noopener").and include("noreferrer")
+ expect(page).to have_link("doc_sample.txt", href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path))
+ .and have_selector(".js-artifact-tree-external-icon")
+
+ page.within(".tree-table") do
+ expect(page).to have_content("..").and have_content("another-subdirectory")
+ end
+
+ page.within(".repo-breadcrumb") do
+ expect(page).to have_content("other_artifacts_0.1.2")
+ end
+ end
+ end
+
+ context "when the project is private" do
+ let!(:private_project) { create(:project, :private) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: private_project) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:user) { create(:user) }
+
+ before do
+ private_project.add_developer(user)
+
+ sign_in(user)
+
+ visit(browse_project_job_artifacts_path(private_project, job, "other_artifacts_0.1.2"))
+ end
+
+ it { expect(page).to have_link("doc_sample.txt").and have_no_selector(".js-artifact-tree-external-icon") }
+ end
+ end
+end
diff --git a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
new file mode 100644
index 00000000000..67ed2f18d76
--- /dev/null
+++ b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb
@@ -0,0 +1,44 @@
+require "spec_helper"
+
+describe "User downloads artifacts" do
+ set(:project) { create(:project, :public) }
+ set(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) }
+ set(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
+
+ shared_examples "downloading" do
+ it "downloads the zip" do
+ expect(page.response_headers["Content-Disposition"]).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"})
+ expect(page.response_headers['Content-Transfer-Encoding']).to eq("binary")
+ expect(page.response_headers['Content-Type']).to eq("application/zip")
+ expect(page.source.b).to eq(job.artifacts_file.file.read.b)
+ end
+ end
+
+ context "when downloading" do
+ before do
+ visit(url)
+ end
+
+ context "via job id" do
+ set(:url) { download_project_job_artifacts_path(project, job) }
+
+ it_behaves_like "downloading"
+ end
+
+ context "via branch name and job name" do
+ set(:url) { latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) }
+
+ it_behaves_like "downloading"
+ end
+
+ context "via clicking the `Download` button" do
+ set(:url) { project_job_path(project, job) }
+
+ before do
+ click_link("Download")
+ end
+
+ it_behaves_like "downloading"
+ end
+ end
+end
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index ac82f869f0f..e7b305925f7 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -14,6 +14,8 @@ feature 'File blob', :js do
context 'Ruby file' do
before do
visit_blob('files/ruby/popen.rb')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -48,6 +50,8 @@ feature 'File blob', :js do
context 'visiting directly' do
before do
visit_blob('files/markdown/ruby-style-guide.md')
+
+ wait_for_requests
end
it 'displays the blob using the rich viewer' do
@@ -159,6 +163,8 @@ feature 'File blob', :js do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/file.md')
+
+ wait_for_requests
end
it 'displays an error' do
@@ -207,6 +213,8 @@ feature 'File blob', :js do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/file.md')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -242,6 +250,8 @@ feature 'File blob', :js do
).execute
visit_blob('files/test.pdf')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -268,6 +278,8 @@ feature 'File blob', :js do
project.update_attribute(:lfs_enabled, true)
visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -290,6 +302,8 @@ feature 'File blob', :js do
context 'when LFS is disabled on the project' do
before do
visit_blob('files/lfs/lfs_object.iso')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -313,6 +327,8 @@ feature 'File blob', :js do
context 'ZIP file' do
before do
visit_blob('Gemfile.zip')
+
+ wait_for_requests
end
it 'displays the blob' do
@@ -347,6 +363,8 @@ feature 'File blob', :js do
).execute
visit_blob('files/empty.md')
+
+ wait_for_requests
end
it 'displays an error' do
diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
new file mode 100644
index 00000000000..b7d063596c1
--- /dev/null
+++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+feature 'User creates blob in new project', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :empty_repo) }
+
+ shared_examples 'creating a file' do
+ before do
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'allows the user to add a new file' do
+ click_link 'New file'
+
+ find('#editor')
+ execute_script('ace.edit("editor").setValue("Hello world")')
+
+ fill_in(:file_name, with: 'dummy-file')
+
+ click_button('Commit changes')
+
+ expect(page).to have_content('The file has been successfully created')
+ end
+ end
+
+ describe 'as a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'creating a file'
+ end
+
+ describe 'as an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it_behaves_like 'creating a file'
+ end
+
+ describe 'as a developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'does not allow pushing to the default branch' do
+ expect(page).not_to have_content('New file')
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index dfe8e02dce0..a8a627d8806 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -182,6 +182,47 @@ feature 'Gcp Cluster', :js do
it 'user sees a login page' do
expect(page).to have_css('.signin-with-google')
+ expect(page).to have_link('Google account')
+ end
+ end
+
+ context 'when user has not dismissed GCP signup offer' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees offer on cluster index page' do
+ expect(page).to have_css('.gcp-signup-offer')
+ end
+
+ it 'user sees offer on cluster create page' do
+ click_link 'Add Kubernetes cluster'
+
+ expect(page).to have_css('.gcp-signup-offer')
+ end
+
+ it 'user sees offer on cluster GCP login page' do
+ click_link 'Add Kubernetes cluster'
+ click_link 'Create on Google Kubernetes Engine'
+
+ expect(page).to have_css('.gcp-signup-offer')
+ end
+ end
+
+ context 'when user has dismissed GCP signup offer' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user does not see offer after dismissing' do
+ expect(page).to have_css('.gcp-signup-offer')
+
+ find('.gcp-signup-offer .close').click
+ wait_for_requests
+
+ click_link 'Add Kubernetes cluster'
+
+ expect(page).not_to have_css('.gcp-signup-offer')
end
end
end
diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
new file mode 100644
index 00000000000..6397df086a7
--- /dev/null
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -0,0 +1,170 @@
+require "spec_helper"
+
+describe "User adds a comment on a commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:comment_text) { "XML attached" }
+ let(:another_comment_text) { "SVG attached" }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ context "inline view" do
+ before do
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ it "adds a comment" do
+ page.within(".js-main-target-form") do
+ expect(page).not_to have_link("Cancel")
+
+ emoji = ":+1:"
+
+ fill_in("note[note]", with: "#{comment_text} #{emoji}")
+
+ # Check on `Preview` tab
+ click_link("Preview")
+
+ expect(find(".js-md-preview")).to have_content(comment_text).and have_css("gl-emoji")
+ expect(page).not_to have_css(".js-note-text")
+
+ # Check on the `Write` tab
+ click_link("Write")
+
+ expect(page).to have_field("note[note]", with: "#{comment_text} #{emoji}")
+
+ # Submit comment from the `Preview` tab to get rid of a separate `it` block
+ # which would specially tests if everything gets cleared from the note form.
+ click_link("Preview")
+ click_button("Comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(comment_text).and have_css("gl-emoji")
+ end
+
+ page.within(".js-main-target-form") do
+ expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
+ end
+ end
+
+ context "when commenting on diff" do
+ it "adds a comment" do
+ page.within(".diff-file:nth-of-type(1)") do
+ # Open a form for a comment and check UI elements are visible and acting as expecting.
+ click_diff_line(sample_commit.line_code)
+
+ expect(page).to have_css(".js-temp-notes-holder form.new-note")
+ .and have_css(".js-close-discussion-note-form", text: "Cancel")
+
+ # The `Cancel` button closes the current form. The page should not have any open forms after that.
+ find(".js-close-discussion-note-form").click
+
+ expect(page).not_to have_css("form.new_note")
+
+ # Try to open the same form twice. There should be only one form opened.
+ click_diff_line(sample_commit.line_code)
+ click_diff_line(sample_commit.line_code)
+
+ expect(page).to have_css("form.new-note", count: 1)
+
+ # Fill in a form.
+ page.within("form[data-line-code='#{sample_commit.line_code}']") do
+ fill_in("note[note]", with: "#{comment_text} :smile:")
+ end
+
+ # Open another form and check we have two forms now (because the first one is filled in).
+ click_diff_line(sample_commit.del_line_code)
+
+ expect(page).to have_field("note[note]", with: "#{comment_text} :smile:")
+ .and have_field("note[note]", with: "")
+
+ # Test Preview feature for both forms.
+ page.within("form[data-line-code='#{sample_commit.line_code}']") do
+ click_link("Preview")
+ end
+
+ page.within("form[data-line-code='#{sample_commit.del_line_code}']") do
+ fill_in("note[note]", with: another_comment_text)
+
+ click_link("Preview")
+ end
+
+ expect(page).to have_css(".js-md-preview", visible: true, count: 2)
+ .and have_content(comment_text)
+ .and have_content(another_comment_text)
+ .and have_xpath("//gl-emoji[@data-name='smile']")
+
+ # Test UI elements, then submit.
+ page.within("form[data-line-code='#{sample_commit.line_code}']") do
+ expect(find(".js-note-text", visible: false).text).to eq("")
+ expect(page).to have_css('.js-md-write-button')
+
+ click_button("Comment")
+ end
+
+ expect(page).to have_button("Reply...").and have_no_css("form.new_note")
+ end
+
+ # A comment should be added and visible.
+ page.within(".diff-file:nth-of-type(1) .note") do
+ expect(page).to have_content(comment_text).and have_xpath("//gl-emoji[@data-name='smile']")
+ end
+ end
+ end
+ end
+
+ context "side-by-side view" do
+ before do
+ visit(project_commit_path(project, sample_commit.id, view: "parallel"))
+ end
+
+ it "adds a comment" do
+ new_comment = "New comment"
+ old_comment = "Old comment"
+
+ # Left side.
+ click_parallel_diff_line(sample_commit.del_line_code)
+
+ page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.del_line_code}']") do
+ fill_in("note[note]", with: old_comment)
+ click_button("Comment")
+ end
+
+ page.within(".diff-file:nth-of-type(1) .notes_content.parallel.old") do
+ expect(page).to have_content(old_comment)
+ end
+
+ # Right side.
+ click_parallel_diff_line(sample_commit.line_code)
+
+ page.within(".diff-file:nth-of-type(1) form[data-line-code='#{sample_commit.line_code}']") do
+ fill_in("note[note]", with: new_comment)
+ click_button("Comment")
+ end
+
+ wait_for_requests
+
+ expect(all(".diff-file:nth-of-type(1) .notes_content.parallel.new")[1].text).to have_content(new_comment)
+ end
+ end
+
+ private
+
+ def click_diff_line(line)
+ find(".line_holder[id='#{line}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line}'] button").click
+ end
+
+ def click_parallel_diff_line(line)
+ find(".line_holder.parallel td[id='#{line}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover
+ find(".line_holder.parallel button[data-line-code='#{line}']").click
+ end
+end
diff --git a/spec/features/projects/commit/comments/user_deletes_comments_spec.rb b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
new file mode 100644
index 00000000000..a727cab4ac7
--- /dev/null
+++ b/spec/features/projects/commit/comments/user_deletes_comments_spec.rb
@@ -0,0 +1,37 @@
+require "spec_helper"
+
+describe "User deletes comments on a commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:comment_text) { "XML attached" }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+
+ add_note(comment_text)
+ end
+
+ it "deletes comment" do
+ page.within(".note") do
+ expect(page).to have_content(comment_text)
+ end
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ find(".more-actions").click
+ find(".more-actions .dropdown-menu li", match: :first)
+
+ accept_confirm { find(".js-note-delete").click }
+ end
+
+ expect(page).not_to have_css(".note")
+ end
+end
diff --git a/spec/features/projects/commit/comments/user_edits_comments_spec.rb b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
new file mode 100644
index 00000000000..75bccd99f59
--- /dev/null
+++ b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
@@ -0,0 +1,42 @@
+require "spec_helper"
+
+describe "User edits a comment on a commit", :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+
+ add_note("XML attached")
+ end
+
+ it "edits comment" do
+ NEW_COMMENT_TEXT = "+1 Awesome!".freeze
+
+ page.within(".main-notes-list") do
+ note = find(".note")
+ note.hover
+
+ note.find(".js-note-edit").click
+ end
+
+ page.find(".current-note-edit-form textarea")
+
+ page.within(".current-note-edit-form") do
+ fill_in("note[note]", with: NEW_COMMENT_TEXT)
+ click_button("Save comment")
+ end
+
+ wait_for_requests
+
+ page.within(".note") do
+ expect(page).to have_content(NEW_COMMENT_TEXT)
+ end
+ end
+end
diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb
deleted file mode 100644
index 5174f793367..00000000000
--- a/spec/features/projects/commit/user_comments_on_commit_spec.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-require "spec_helper"
-
-describe "User comments on commit", :js do
- include Spec::Support::Helpers::Features::NotesHelpers
- include RepoHelpers
-
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
-
- COMMENT_TEXT = "XML attached".freeze
-
- before do
- sign_in(user)
- project.add_developer(user)
-
- visit(project_commit_path(project, sample_commit.id))
- end
-
- context "when adding new comment" do
- it "adds comment" do
- EMOJI = ":+1:".freeze
-
- page.within(".js-main-target-form") do
- expect(page).not_to have_link("Cancel")
-
- fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
-
- # Check on `Preview` tab
- click_link("Preview")
-
- expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
- expect(page).not_to have_css(".js-note-text")
-
- # Check on `Write` tab
- click_link("Write")
-
- expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}")
-
- # Submit comment from the `Preview` tab to get rid of a separate `it` block
- # which would specially tests if everything gets cleared from the note form.
- click_link("Preview")
- click_button("Comment")
- end
-
- wait_for_requests
-
- page.within(".note") do
- expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji")
- end
-
- page.within(".js-main-target-form") do
- expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview")
- end
- end
- end
-
- context "when editing comment" do
- before do
- add_note(COMMENT_TEXT)
- end
-
- it "edits comment" do
- NEW_COMMENT_TEXT = "+1 Awesome!".freeze
-
- page.within(".main-notes-list") do
- note = find(".note")
- note.hover
-
- note.find(".js-note-edit").click
- end
-
- page.find(".current-note-edit-form textarea")
-
- page.within(".current-note-edit-form") do
- fill_in("note[note]", with: NEW_COMMENT_TEXT)
- click_button("Save comment")
- end
-
- wait_for_requests
-
- page.within(".note") do
- expect(page).to have_content(NEW_COMMENT_TEXT)
- end
- end
- end
-
- context "when deleting comment" do
- before do
- add_note(COMMENT_TEXT)
- end
-
- it "deletes comment" do
- page.within(".note") do
- expect(page).to have_content(COMMENT_TEXT)
- end
-
- page.within(".main-notes-list") do
- note = find(".note")
- note.hover
-
- find(".more-actions").click
- find(".more-actions .dropdown-menu li", match: :first)
-
- accept_confirm { find(".js-note-delete").click }
- end
-
- expect(page).not_to have_css(".note")
- end
- end
-end
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index b650c1f4197..35ed6620548 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'User browses commits' do
+ include RepoHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
@@ -9,13 +11,68 @@ describe 'User browses commits' do
sign_in(user)
end
+ it 'renders commit' do
+ visit project_commit_path(project, sample_commit.id)
+
+ expect(page).to have_content(sample_commit.message)
+ .and have_content("Showing #{sample_commit.files_changed_count} changed files")
+ .and have_content('Side-by-side')
+ end
+
+ it 'fill commit sha when click new tag from commit page' do
+ visit project_commit_path(project, sample_commit.id)
+ click_link 'Tag'
+
+ expect(page).to have_selector("input[value='#{sample_commit.id}']", visible: false)
+ end
+
+ it 'renders inline diff button when click side-by-side diff button' do
+ visit project_commit_path(project, sample_commit.id)
+ find('#parallel-diff-btn').click
+
+ expect(page).to have_content 'Inline'
+ end
+
+ it 'renders breadcrumbs on specific commit path' do
+ visit project_commits_path(project, project.repository.root_ref + '/files/ruby/regex.rb', limit: 5)
+
+ expect(page).to have_selector('ul.breadcrumb')
+ .and have_selector('ul.breadcrumb a', count: 4)
+ end
+
+ it 'renders diff links to both the previous and current image' do
+ visit project_commit_path(project, sample_image_commit.id)
+
+ links = page.all('.file-actions a')
+ expect(links[0]['href']).to match %r{blob/#{sample_image_commit.old_blob_id}}
+ expect(links[1]['href']).to match %r{blob/#{sample_image_commit.new_blob_id}}
+ end
+
+ context 'when commit has ci status' do
+ let(:pipeline) { create(:ci_pipeline, project: project, sha: sample_commit.id) }
+
+ before do
+ project.enable_ci
+
+ create(:ci_build, pipeline: pipeline)
+
+ allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return('')
+ end
+
+ it 'renders commit ci info' do
+ visit project_commit_path(project, sample_commit.id)
+
+ expect(page).to have_content "Pipeline ##{pipeline.id} pending"
+ end
+ end
+
context 'primary email' do
it 'finds a commit by a primary email' do
user = create(:user, email: 'dmitriy.zaporozhets@gmail.com')
- visit(project_commit_path(project, RepoHelpers.sample_commit.id))
+ visit(project_commit_path(project, sample_commit.id))
- check_author_link(RepoHelpers.sample_commit.author_email, user)
+ check_author_link(sample_commit.author_email, user)
end
end
@@ -26,9 +83,9 @@ describe 'User browses commits' do
create(:email, { user: user, email: 'dmitriy.zaporozhets@gmail.com' })
end
- visit(project_commit_path(project, RepoHelpers.sample_commit.parent_id))
+ visit(project_commit_path(project, sample_commit.parent_id))
- check_author_link(RepoHelpers.sample_commit.author_email, user)
+ check_author_link(sample_commit.author_email, user)
end
end
@@ -44,6 +101,135 @@ describe 'User browses commits' do
expect(find('.diff-file-changes', visible: false)).to have_content('No file name available')
end
end
+
+ describe 'commits list' do
+ let(:visit_commits_page) do
+ visit project_commits_path(project, project.repository.root_ref, limit: 5)
+ end
+
+ it 'searches commit', :js do
+ visit_commits_page
+ fill_in 'commits-search', with: 'submodules'
+
+ expect(page).to have_content 'More submodules'
+ expect(page).not_to have_content 'Change some files'
+ end
+
+ it 'renders commits atom feed' do
+ visit_commits_page
+ click_link('Commits feed')
+
+ commit = project.repository.commit
+
+ expect(response_headers['Content-Type']).to have_content("application/atom+xml")
+ expect(body).to have_selector('title', text: "#{project.name}:master commits")
+ .and have_selector('author email', text: commit.author_email)
+ .and have_selector('entry summary', text: commit.description[0..10].delete("\r\n"))
+ end
+
+ context 'master branch' do
+ before do
+ visit_commits_page
+ end
+
+ it 'renders project commits' do
+ commit = project.repository.commit
+
+ expect(page).to have_content(project.name)
+ .and have_content(commit.message[0..20])
+ .and have_content(commit.short_id)
+ end
+
+ it 'does not render create merge request button' do
+ expect(page).not_to have_link 'Create merge request'
+ end
+
+ context 'when click the compare tab' do
+ before do
+ click_link('Compare')
+ end
+
+ it 'does not render create merge request button' do
+ expect(page).not_to have_link 'Create merge request'
+ end
+ end
+ end
+
+ context 'feature branch' do
+ let(:visit_commits_page) do
+ visit project_commits_path(project, 'feature')
+ end
+
+ context 'when project does not have open merge requests' do
+ before do
+ visit_commits_page
+ end
+
+ it 'renders project commits' do
+ commit = project.repository.commit('0b4bc9a')
+
+ expect(page).to have_content(project.name)
+ .and have_content(commit.message[0..12])
+ .and have_content(commit.short_id)
+ end
+
+ it 'renders create merge request button' do
+ expect(page).to have_link 'Create merge request'
+ end
+
+ context 'when click the compare tab' do
+ before do
+ click_link('Compare')
+ end
+
+ it 'renders create merge request button' do
+ expect(page).to have_link 'Create merge request'
+ end
+ end
+ end
+
+ context 'when project have open merge request' do
+ let!(:merge_request) do
+ create(
+ :merge_request,
+ title: 'Feature',
+ source_project: project,
+ source_branch: 'feature',
+ target_branch: 'master',
+ author: project.users.first
+ )
+ end
+
+ before do
+ visit_commits_page
+ end
+
+ it 'renders project commits' do
+ commit = project.repository.commit('0b4bc9a')
+
+ expect(page).to have_content(project.name)
+ .and have_content(commit.message[0..12])
+ .and have_content(commit.short_id)
+ end
+
+ it 'renders button to the merge request' do
+ expect(page).not_to have_link 'Create merge request'
+ expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request)
+ end
+
+ context 'when click the compare tab' do
+ before do
+ click_link('Compare')
+ end
+
+ it 'renders button to the merge request' do
+ expect(page).not_to have_link 'Create merge request'
+ expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request)
+ end
+ end
+ end
+ end
+ end
end
private
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 1fb22fd0e4c..7e863d9df32 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -7,16 +7,19 @@ describe "Compare", :js do
before do
project.add_master(user)
sign_in user
- visit project_compare_index_path(project, from: "master", to: "master")
end
describe "branches" do
it "pre-populates fields" do
+ visit project_compare_index_path(project, from: "master", to: "master")
+
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
end
it "compares branches" do
+ visit project_compare_index_path(project, from: "master", to: "master")
+
select_using_dropdown "from", "feature"
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
@@ -26,9 +29,58 @@ describe "Compare", :js do
click_button "Compare"
expect(page).to have_content "Commits"
+ expect(page).to have_link 'Create merge request'
+ end
+
+ it 'renders additions info when click unfold diff' do
+ visit project_compare_index_path(project)
+
+ select_using_dropdown('from', RepoHelpers.sample_commit.parent_id, commit: true)
+ select_using_dropdown('to', RepoHelpers.sample_commit.id, commit: true)
+
+ click_button 'Compare'
+ expect(page).to have_content 'Commits (1)'
+ expect(page).to have_content "Showing 2 changed files"
+
+ diff = first('.js-unfold')
+ diff.click
+ wait_for_requests
+
+ page.within diff.query_scope do
+ expect(first('.new_line').text).not_to have_content "..."
+ end
+ end
+
+ context 'when project have an open merge request' do
+ let!(:merge_request) do
+ create(
+ :merge_request,
+ title: 'Feature',
+ source_project: project,
+ source_branch: 'feature',
+ target_branch: 'master',
+ author: project.users.first
+ )
+ end
+
+ it 'compares branches' do
+ visit project_compare_index_path(project)
+
+ select_using_dropdown('from', 'master')
+ select_using_dropdown('to', 'feature')
+
+ click_button 'Compare'
+
+ expect(page).to have_content 'Commits (1)'
+ expect(page).to have_content 'Showing 1 changed file with 5 additions and 0 deletions'
+ expect(page).to have_link 'View open merge request', href: project_merge_request_path(project, merge_request)
+ expect(page).not_to have_link 'Create merge request'
+ end
end
it "filters branches" do
+ visit project_compare_index_path(project, from: "master", to: "master")
+
select_using_dropdown("from", "wip")
find(".js-compare-from-dropdown .compare-dropdown-toggle").click
@@ -39,6 +91,8 @@ describe "Compare", :js do
describe "tags" do
it "compares tags" do
+ visit project_compare_index_path(project, from: "master", to: "master")
+
select_using_dropdown "from", "v1.0.0"
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
@@ -50,15 +104,20 @@ describe "Compare", :js do
end
end
- def select_using_dropdown(dropdown_type, selection)
+ def select_using_dropdown(dropdown_type, selection, commit: false)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
# find input before using to wait for the inputs visiblity
dropdown.find('.dropdown-menu')
dropdown.fill_in("Filter by Git revision", with: selection)
wait_for_requests
- # find before all to wait for the items visiblity
- dropdown.find("a[data-ref=\"#{selection}\"]", match: :first)
- dropdown.all("a[data-ref=\"#{selection}\"]").last.click
+
+ if commit
+ dropdown.find('input[type="search"]').send_keys(:return)
+ else
+ # find before all to wait for the items visiblity
+ dropdown.find("a[data-ref=\"#{selection}\"]", match: :first)
+ dropdown.all("a[data-ref=\"#{selection}\"]").last.click
+ end
end
end
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 886c56e7163..43a23c42f83 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -18,12 +18,12 @@ describe 'Project deploy keys', :js do
visit project_settings_repository_path(project)
page.within(find('.deploy-keys')) do
- expect(page).to have_selector('.deploy-keys li', count: 1)
+ expect(page).to have_selector('.deploy-key', count: 1)
- accept_confirm { find(:button, text: 'Remove').send_keys(:return) }
+ accept_confirm { find('.ic-remove').click() }
expect(page).not_to have_selector('.fa-spinner', count: 0)
- expect(page).to have_selector('.deploy-keys li', count: 0)
+ expect(page).to have_selector('.deploy-key', count: 0)
end
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 5248a783db4..f9defa22d35 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -42,6 +42,22 @@ feature 'Environments page', :js do
expect(page).to have_content('You don\'t have any environments right now')
end
end
+
+ context 'when cluster is not reachable' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let!(:application_prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+
+ before do
+ allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ end
+
+ it 'should show one environment without error' do
+ visit_environments(project, scope: 'available')
+
+ expect(page).to have_css('.environments-container')
+ expect(page.all('.environment-name').length).to eq(1)
+ end
+ end
end
describe 'with one stopped environment' do
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 9c1f11f4c12..41f6c52fb8a 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -1,14 +1,12 @@
-require 'spec_helper'
+require "spec_helper"
-describe 'Projects > Files > User browses files' do
+describe "User browses files" do
let(:fork_message) do
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
- let(:project) { create(:project, :repository, name: 'Shop') }
- let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') }
- let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
- let(:tree_path_ref_6d39438) { project_tree_path(project, '6d39438') }
+ let(:project) { create(:project, :repository, name: "Shop") }
+ let(:project2) { create(:project, :repository, name: "Another Project", path: "another-project") }
let(:tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) }
let(:user) { project.owner }
@@ -16,57 +14,55 @@ describe 'Projects > Files > User browses files' do
sign_in(user)
end
- it 'shows last commit for current directory' do
+ it "shows last commit for current directory" do
visit(tree_path_root_ref)
- click_link 'files'
+ click_link("files")
- last_commit = project.repository.last_commit_for_path(project.default_branch, 'files')
- page.within('.blob-commit-info') do
- expect(page).to have_content last_commit.short_id
- expect(page).to have_content last_commit.author_name
+ last_commit = project.repository.last_commit_for_path(project.default_branch, "files")
+
+ page.within(".blob-commit-info") do
+ expect(page).to have_content(last_commit.short_id).and have_content(last_commit.author_name)
end
end
- context 'when browsing the master branch' do
+ context "when browsing the master branch" do
before do
visit(tree_path_root_ref)
end
- it 'shows files from a repository' do
- expect(page).to have_content('VERSION')
- expect(page).to have_content('.gitignore')
- expect(page).to have_content('LICENSE')
+ it "shows files from a repository" do
+ expect(page).to have_content("VERSION")
+ .and have_content(".gitignore")
+ .and have_content("LICENSE")
end
- it 'shows the "Browse Directory" link' do
- click_link('files')
- click_link('History')
+ it "shows the `Browse Directory` link" do
+ click_link("files")
+ click_link("History")
- expect(page).to have_link('Browse Directory')
- expect(page).not_to have_link('Browse Code')
+ expect(page).to have_link("Browse Directory").and have_no_link("Browse Code")
end
- it 'shows the "Browse File" link' do
- page.within('.tree-table') do
- click_link('README.md')
+ it "shows the `Browse File` link" do
+ page.within(".tree-table") do
+ click_link("README.md")
end
- click_link('History')
- expect(page).to have_link('Browse File')
- expect(page).not_to have_link('Browse Files')
+ click_link("History")
+
+ expect(page).to have_link("Browse File").and have_no_link("Browse Files")
end
- it 'shows the "Browse Files" link' do
- click_link('History')
+ it "shows the `Browse Files` link" do
+ click_link("History")
- expect(page).to have_link('Browse Files')
- expect(page).not_to have_link('Browse Directory')
+ expect(page).to have_link("Browse Files").and have_no_link("Browse Directory")
end
- it 'redirects to the permalink URL' do
- click_link('.gitignore')
- click_link('Permalink')
+ it "redirects to the permalink URL" do
+ click_link(".gitignore")
+ click_link("Permalink")
permalink_path = project_blob_path(project, "#{project.repository.commit.sha}/.gitignore")
@@ -74,80 +70,180 @@ describe 'Projects > Files > User browses files' do
end
end
- context 'when browsing a specific ref' do
+ context "when browsing the `markdown` branch", :js do
+ context "when browsing the root" do
+ before do
+ visit(project_tree_path(project, "markdown"))
+ end
+
+ it "shows correct files and links" do
+ # rubocop:disable Lint/Void
+ # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
+ find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown")
+ find("a", text: /^#id$/)["href"] == project_tree_url(project, "markdown", anchor: "#id")
+ find("a", text: %r{^/#id$})["href"] == project_tree_url(project, "markdown", anchor: "#id")
+ find("a", text: /^README.md#id$/)["href"] == project_blob_url(project, "markdown/README.md", anchor: "#id")
+ find("a", text: %r{^d/README.md#id$})["href"] == project_blob_url(project, "d/markdown/README.md", anchor: "#id")
+ # rubocop:enable Lint/Void
+
+ expect(current_path).to eq(project_tree_path(project, "markdown"))
+ expect(page).to have_content("README.md")
+ .and have_content("CHANGELOG")
+ .and have_content("Welcome to GitLab GitLab is a free project and repository management application")
+ .and have_link("GitLab API doc")
+ .and have_link("GitLab API website")
+ .and have_link("Rake tasks")
+ .and have_link("backup and restore procedure")
+ .and have_link("GitLab API doc directory")
+ .and have_link("Maintenance")
+ .and have_header_with_correct_id_and_link(2, "Application details", "application-details")
+ end
+
+ it "shows correct content of file" do
+ click_link("GitLab API doc")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/README.md"))
+ expect(page).to have_content("All API requests require authentication")
+ .and have_content("Contents")
+ .and have_link("Users")
+ .and have_link("Rake tasks")
+ .and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api")
+
+ click_link("Users")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/users.md"))
+ expect(page).to have_content("Get a list of users.")
+
+ page.go_back
+
+ click_link("Rake tasks")
+
+ expect(current_path).to eq(project_tree_path(project, "markdown/doc/raketasks"))
+ expect(page).to have_content("backup_restore.md").and have_content("maintenance.md")
+
+ click_link("shop")
+ click_link("Maintenance")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/raketasks/maintenance.md"))
+ expect(page).to have_content("bundle exec rake gitlab:env:info RAILS_ENV=production")
+
+ click_link("shop")
+
+ page.within(".tree-table") do
+ click_link("README.md")
+ end
+
+ page.go_back
+
+ page.within(".tree-table") do
+ click_link("d")
+ end
+
+ # rubocop:disable Lint/Void
+ # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
+ find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown/d")
+ # rubocop:enable Lint/Void
+
+ page.within(".tree-table") do
+ click_link("README.md")
+ end
+
+ # rubocop:disable Lint/Void
+ # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
+ find("a", text: /^empty$/)["href"] == project_blob_url(project, "markdown/d/README.md")
+ # rubocop:enable Lint/Void
+ end
+
+ it "shows correct content of directory" do
+ click_link("GitLab API doc directory")
+
+ expect(current_path).to eq(project_tree_path(project, "markdown/doc/api"))
+ expect(page).to have_content("README.md").and have_content("users.md")
+
+ click_link("Users")
+
+ expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/users.md"))
+ expect(page).to have_content("List users").and have_content("Get a list of users.")
+ end
+ end
+ end
+
+ context "when browsing a specific ref" do
+ let(:ref) { project_tree_path(project, "6d39438") }
+
before do
- visit(tree_path_ref_6d39438)
+ visit(ref)
end
- it 'shows files from a repository for "6d39438"' do
- expect(current_path).to eq(tree_path_ref_6d39438)
- expect(page).to have_content('.gitignore')
- expect(page).to have_content('LICENSE')
+ it "shows files from a repository for `6d39438`" do
+ expect(current_path).to eq(ref)
+ expect(page).to have_content(".gitignore").and have_content("LICENSE")
end
- it 'shows files from a repository with apostroph in its name', :js do
- first('.js-project-refs-dropdown').click
+ it "shows files from a repository with apostroph in its name", :js do
+ first(".js-project-refs-dropdown").click
- page.within('.project-refs-form') do
+ page.within(".project-refs-form") do
click_link("'test'")
end
- expect(page).to have_selector('.dropdown-toggle-text', text: "'test'")
+ expect(page).to have_selector(".dropdown-toggle-text", text: "'test'")
visit(project_tree_path(project, "'test'"))
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
+ expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
end
- it 'shows the code with a leading dot in the directory', :js do
- first('.js-project-refs-dropdown').click
+ it "shows the code with a leading dot in the directory", :js do
+ first(".js-project-refs-dropdown").click
- page.within('.project-refs-form') do
- click_link('fix')
+ page.within(".project-refs-form") do
+ click_link("fix")
end
- visit(project_tree_path(project, 'fix/.testdir'))
+ visit(project_tree_path(project, "fix/.testdir"))
- expect(page).to have_css('.tree-commit-link', visible: true)
- expect(page).not_to have_content('Loading commit data...')
+ expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
end
- it 'does not show the permalink link' do
- click_link('.gitignore')
+ it "does not show the permalink link" do
+ click_link(".gitignore")
- expect(page).not_to have_link('permalink')
+ expect(page).not_to have_link("permalink")
end
end
- context 'when browsing a file content' do
+ context "when browsing a file content" do
before do
visit(tree_path_root_ref)
- click_link('.gitignore')
+
+ click_link(".gitignore")
end
- it 'shows a file content', :js do
- wait_for_requests
- expect(page).to have_content('*.rbc')
+ it "shows a file content", :js do
+ expect(page).to have_content("*.rbc")
end
- it 'is possible to blame' do
- click_link 'Blame'
+ it "is possible to blame" do
+ click_link("Blame")
- expect(page).to have_content "*.rb"
- expect(page).to have_content "Dmitriy Zaporozhets"
- expect(page).to have_content "Initial commit"
+ expect(page).to have_content("*.rb")
+ .and have_content("Dmitriy Zaporozhets")
+ .and have_content("Initial commit")
end
end
- context 'when browsing a raw file' do
+ context "when browsing a raw file" do
before do
- visit(project_blob_path(project, File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path)))
+ path = File.join(RepoHelpers.sample_commit.id, RepoHelpers.sample_blob.path)
+
+ visit(project_blob_path(project, path))
end
- it 'shows a raw file content' do
- click_link('Open raw')
- expect(source).to eq('') # Body is filled in by gitlab-workhorse
+ it "shows a raw file content" do
+ click_link("Open raw")
+
+ expect(source).to eq("") # Body is filled in by gitlab-workhorse
end
end
end
diff --git a/spec/features/projects/files/user_find_file_spec.rb b/spec/features/projects/files/user_find_file_spec.rb
new file mode 100644
index 00000000000..df405e70dd4
--- /dev/null
+++ b/spec/features/projects/files/user_find_file_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe 'User find project file' do
+ let(:user) { create :user }
+ let(:project) { create :project, :repository }
+
+ before do
+ sign_in(user)
+ project.add_master(user)
+
+ visit project_tree_path(project, project.repository.root_ref)
+ end
+
+ def active_main_tab
+ find('.sidebar-top-level-items > li.active')
+ end
+
+ def find_file(text)
+ fill_in 'file_find', with: text
+ end
+
+ it 'navigates to find file by shortcut', :js do
+ find('body').native.send_key('t')
+
+ expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ it 'navigates to find file' do
+ click_link 'Find file'
+
+ expect(active_main_tab).to have_content('Repository')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ it 'searches CHANGELOG file', :js do
+ click_link 'Find file'
+
+ find_file 'change'
+
+ expect(page).to have_content('CHANGELOG')
+ expect(page).not_to have_content('.gitignore')
+ expect(page).not_to have_content('VERSION')
+ end
+
+ it 'does not find file when search not exist file', :js do
+ click_link 'Find file'
+
+ find_file 'asdfghjklqwertyuizxcvbnm'
+
+ expect(page).not_to have_content('CHANGELOG')
+ expect(page).not_to have_content('.gitignore')
+ expect(page).not_to have_content('VERSION')
+ end
+
+ it 'searches file by partially matches', :js do
+ click_link 'Find file'
+
+ find_file 'git'
+
+ expect(page).to have_content('.gitignore')
+ expect(page).to have_content('.gitmodules')
+ expect(page).not_to have_content('CHANGELOG')
+ expect(page).not_to have_content('VERSION')
+ end
+end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index b25f5161748..60fe30bd898 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -46,7 +46,7 @@ feature 'Import/Export - project import integration test', :js do
expect(project.merge_requests).not_to be_empty
expect(project_hook_exists?(project)).to be true
expect(wiki_exists?(project)).to be true
- expect(project.import_status).to eq('finished')
+ expect(project.import_state.status).to eq('finished')
end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index ecb7651acad..72ab2d71f35 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issues/user_sorts_issues_spec.rb b/spec/features/projects/issues/user_sorts_issues_spec.rb
index c3d63000dac..db5936a30cb 100644
--- a/spec/features/projects/issues/user_sorts_issues_spec.rb
+++ b/spec/features/projects/issues/user_sorts_issues_spec.rb
@@ -18,7 +18,7 @@ describe "User sorts issues" do
it "sorts by popularity" do
find("button.dropdown-toggle").click
- page.within(".content ul.dropdown-menu.dropdown-menu-align-right li") do
+ page.within(".content ul.dropdown-menu.dropdown-menu-right li") do
click_link("Popularity")
end
diff --git a/spec/features/projects/issues/user_toggles_subscription_spec.rb b/spec/features/projects/issues/user_toggles_subscription_spec.rb
index 117a614b980..c2b2a193682 100644
--- a/spec/features/projects/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/projects/issues/user_toggles_subscription_spec.rb
@@ -1,9 +1,9 @@
require "spec_helper"
describe "User toggles subscription", :js do
- set(:project) { create(:project_empty_repo, :public) }
- set(:user) { create(:user) }
- set(:issue) { create(:issue, project: project, author: user) }
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: user) }
before do
project.add_developer(user)
@@ -12,7 +12,7 @@ describe "User toggles subscription", :js do
visit(project_issue_path(project, issue))
end
- it "unsibscribes from issue" do
+ it "unsubscribes from issue" do
subscription_button = find(".js-issuable-subscribe-button")
# Check we're subscribed.
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index 36ebbeadd4a..786ec327b92 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -26,7 +26,7 @@ describe 'User browses jobs' do
page.within('.nav-controls') do
ci_lint_tool_link = page.find_link('CI lint')
- expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path)
+ expect(ci_lint_tool_link[:href]).to end_with(project_ci_lint_path(project))
end
end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index a460024542c..9d1c4cbad8b 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'tempfile'
-feature 'Jobs' do
+feature 'Jobs', :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project, :repository) }
@@ -282,7 +282,7 @@ feature 'Jobs' do
it 'loads job trace' do
expect(page).to have_content 'BUILD TRACE'
- job.trace.write do |stream|
+ job.trace.write('a+b') do |stream|
stream.append(' and more trace', 11)
end
@@ -491,16 +491,18 @@ feature 'Jobs' do
end
end
- describe "POST /:project/jobs/:id/retry" do
+ describe "POST /:project/jobs/:id/retry", :js do
context "Job from project", :js do
before do
job.run!
+ job.cancel!
visit project_job_path(project, job)
- find('.js-cancel-job').click()
+ wait_for_requests
+
find('.js-retry-button').click
end
- it 'shows the right status and buttons', :js do
+ it 'shows the right status and buttons' do
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
end
@@ -591,44 +593,6 @@ feature 'Jobs' do
end
end
- context 'storage form' do
- let(:existing_file) { Tempfile.new('existing-trace-file').path }
-
- before do
- job.run!
- end
-
- context 'when job has trace in file', :js do
- before do
- allow_any_instance_of(Gitlab::Ci::Trace)
- .to receive(:paths)
- .and_return([existing_file])
- end
-
- it 'sends the right headers' do
- requests = inspect_requests(inject_headers: { 'X-Sendfile-Type' => 'X-Sendfile' }) do
- visit raw_project_job_path(project, job)
- end
- expect(requests.first.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(requests.first.response_headers['X-Sendfile']).to eq(existing_file)
- end
- end
-
- context 'when job has trace in the database', :js do
- before do
- allow_any_instance_of(Gitlab::Ci::Trace)
- .to receive(:paths)
- .and_return([])
-
- visit project_job_path(project, job)
- end
-
- it 'sends the right headers' do
- expect(page).not_to have_selector('.js-raw-link-controller')
- end
- end
- end
-
context "when visiting old URL" do
let(:raw_job_url) do
raw_project_job_path(project, job)
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index 1f4eec0a317..3ac6ca4fc86 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -1,47 +1,8 @@
require 'spec_helper'
feature 'Projects > Members > Master manages access requests' do
- let(:user) { create(:user) }
- let(:master) { create(:user) }
- let(:project) { create(:project, :public, :access_requestable) }
-
- background do
- project.request_access(user)
- project.add_master(master)
- sign_in(master)
- end
-
- scenario 'master can see access requests' do
- visit project_project_members_path(project)
-
- expect_visible_access_request(project, user)
- end
-
- scenario 'master can grant access' do
- visit project_project_members_path(project)
-
- expect_visible_access_request(project, user)
-
- perform_enqueued_jobs { click_on 'Grant access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was granted"
- end
-
- scenario 'master can deny access' do
- visit project_project_members_path(project)
-
- expect_visible_access_request(project, user)
-
- perform_enqueued_jobs { click_on 'Deny access' }
-
- expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was denied"
- end
-
- def expect_visible_access_request(project, user)
- expect(project.requesters.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "Users requesting access to #{project.name} 1"
- expect(page).to have_content user.name
+ it_behaves_like 'Master manages access requests' do
+ let(:entity) { create(:project, :public, :access_requestable) }
+ let(:members_page_path) { project_project_members_path(entity) }
end
end
diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
index c35ba2d7016..01aeed93947 100644
--- a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
@@ -10,6 +10,15 @@ describe 'User accepts a merge request', :js do
sign_in(user)
end
+ it 'presents merged merge request content' do
+ visit(merge_request_path(merge_request))
+
+ click_button('Merge')
+
+ expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with \
+ #{merge_request.short_merge_commit_sha}")
+ end
+
context 'with removing the source branch' do
before do
visit(merge_request_path(merge_request))
diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
index f285c6c8783..1f21ef7b382 100644
--- a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
@@ -1,32 +1,84 @@
-require 'spec_helper'
+require "spec_helper"
-describe 'User creates a merge request', :js do
+describe "User creates a merge request", :js do
+ include ProjectForksHelper
+
+ let(:title) { "Some feature" }
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
project.add_master(user)
sign_in(user)
+ end
+ it "creates a merge request" do
visit(project_new_merge_request_path(project))
- end
- it 'creates a merge request' do
- find('.js-source-branch').click
- click_link('fix')
+ find(".js-source-branch").click
+ click_link("fix")
- find('.js-target-branch').click
- click_link('feature')
+ find(".js-target-branch").click
+ click_link("feature")
- click_button('Compare branches')
+ click_button("Compare branches")
- fill_in('merge_request_title', with: 'Wiki Feature')
- click_button('Submit merge request')
+ fill_in("Title", with: title)
+ click_button("Submit merge request")
- page.within('.merge-request') do
- expect(page).to have_content('Wiki Feature')
+ page.within(".merge-request") do
+ expect(page).to have_content(title)
end
+ end
+
+ context "to a forked project" do
+ let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
+
+ it "creates a merge request" do
+ visit(project_new_merge_request_path(forked_project))
+
+ expect(page).to have_content("Source branch").and have_content("Target branch")
+ expect(find("#merge_request_target_project_id", visible: false).value).to eq(project.id.to_s)
+
+ click_button("Compare branches and continue")
+
+ expect(page).to have_content("You must select source and target branch")
+
+ first(".js-source-project").click
+ first(".dropdown-source-project a", text: forked_project.full_path)
+
+ first(".js-target-project").click
+ first(".dropdown-target-project a", text: project.full_path)
+
+ first(".js-source-branch").click
- wait_for_requests
+ wait_for_requests
+
+ source_branch = "fix"
+
+ first(".js-source-branch-dropdown .dropdown-content a", text: source_branch).click
+
+ click_button("Compare branches and continue")
+
+ expect(page).to have_css("h3.page-title", text: "New Merge Request")
+
+ page.within("form#new_merge_request") do
+ fill_in("Title", with: title)
+ end
+
+ click_button("Assignee")
+
+ expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
+
+ page.within(".dropdown-menu-user") do
+ expect(page).to have_content("Unassigned")
+ .and have_content(user.name)
+ .and have_content(project.users.first.name)
+ end
+
+ click_button("Submit merge request")
+
+ expect(page).to have_content(title).and have_content("Request to merge #{user.namespace.name}:#{source_branch} into master")
+ end
end
end
diff --git a/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb b/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb
new file mode 100644
index 00000000000..6539e6e9208
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_merges_merge_request_spec.rb
@@ -0,0 +1,43 @@
+require "spec_helper"
+
+describe "User merges a merge request", :js do
+ let(:user) { project.owner }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples "fast forward merge a merge request" do
+ it "merges a merge request" do
+ expect(page).to have_content("Fast-forward merge without a merge commit").and have_button("Merge")
+
+ page.within(".mr-state-widget") do
+ click_button("Merge")
+ end
+
+ page.within(".status-box") do
+ expect(page).to have_content("Merged")
+ end
+ end
+ end
+
+ context "ff-only merge" do
+ let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ context "when branch is rebased" do
+ let!(:merge_request) { create(:merge_request, :rebased, source_project: project) }
+
+ it_behaves_like "fast forward merge a merge request"
+ end
+
+ context "when branch is merged" do
+ let!(:merge_request) { create(:merge_request, :merged_target, source_project: project) }
+
+ it_behaves_like "fast forward merge a merge request"
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb b/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb
new file mode 100644
index 00000000000..92e1c9942b1
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_rebases_merge_request_spec.rb
@@ -0,0 +1,34 @@
+require "spec_helper"
+
+describe "User rebases a merge request", :js do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:user) { project.owner }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples "rebases" do
+ it "rebases" do
+ visit(merge_request_path(merge_request))
+
+ expect(page).to have_button("Rebase")
+
+ click_button("Rebase")
+
+ expect(page).to have_content("Rebase in progress")
+ end
+ end
+
+ context "when merge is regular" do
+ let(:project) { create(:project, :public, :repository, merge_requests_rebase_enabled: true) }
+
+ it_behaves_like "rebases"
+ end
+
+ context "when merge is ff-only" do
+ let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) }
+
+ it_behaves_like "rebases"
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
index d8d9f7e2a8c..305658f1b5d 100644
--- a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
@@ -18,7 +18,7 @@ describe 'User sorts merge requests' do
it 'keeps the sort option' do
find('button.dropdown-toggle').click
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Last updated')
end
@@ -43,7 +43,7 @@ describe 'User sorts merge requests' do
it 'sorts by popularity' do
find('button.dropdown-toggle').click
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Popularity')
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index a5954fec54b..fee6287558e 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -64,7 +64,7 @@ feature 'New project' do
end
context 'with group namespace' do
- let(:group) { create(:group, :private, owner: user) }
+ let(:group) { create(:group, :private) }
before do
group.add_owner(user)
@@ -81,7 +81,7 @@ feature 'New project' do
end
context 'with subgroup namespace' do
- let(:group) { create(:group, owner: user) }
+ let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) }
before do
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 990e5c4d9df..af2a9567a47 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -3,10 +3,11 @@ require 'spec_helper'
describe 'Pipeline', :js do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:role) { :developer }
before do
sign_in(user)
- project.add_developer(user)
+ project.add_role(user, role)
end
shared_context 'pipeline builds' do
@@ -153,9 +154,10 @@ describe 'Pipeline', :js do
end
context 'page tabs' do
- it 'shows Pipeline and Jobs tabs with link' do
+ it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
+ expect(page).to have_link('Failed Jobs')
end
it 'shows counter in Jobs tab' do
@@ -163,7 +165,17 @@ describe 'Pipeline', :js do
end
it 'shows Pipeline tab as active' do
- expect(page).to have_css('.js-pipeline-tab-link.active')
+ expect(page).to have_css('.js-pipeline-tab-link .active')
+ end
+
+ context 'without permission to access builds' do
+ let(:project) { create(:project, :public, :repository, public_builds: false) }
+ let(:role) { :guest }
+
+ it 'does not show failed jobs tab pane' do
+ expect(page).to have_link('Pipeline')
+ expect(page).not_to have_content('Failed Jobs')
+ end
end
end
@@ -259,7 +271,7 @@ describe 'Pipeline', :js do
end
it 'shows Jobs tab as active' do
- expect(page).to have_css('li.js-builds-tab-link.active')
+ expect(page).to have_css('li.js-builds-tab-link .active')
end
end
@@ -308,8 +320,7 @@ describe 'Pipeline', :js do
end
describe 'GET /:project/pipelines/:id/failures' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') }
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
@@ -340,11 +351,39 @@ describe 'Pipeline', :js do
visit pipeline_failures_page
end
- it 'includes failed jobs' do
+ it 'shows jobs tab pane as active' do
+ expect(page).to have_content('Failed Jobs')
+ expect(page).to have_css('#js-tab-failures.active')
+ end
+
+ it 'lists failed builds' do
+ expect(page).to have_content(failed_build.name)
+ expect(page).to have_content(failed_build.stage)
+ end
+
+ it 'does not show trace' do
expect(page).to have_content('No job trace')
end
end
+ context 'without permission to access builds' do
+ let(:role) { :guest }
+
+ before do
+ project.update(public_builds: false)
+ end
+
+ context 'when accessing failed jobs page' do
+ before do
+ visit pipeline_failures_page
+ end
+
+ it 'fails to access the page' do
+ expect(page).to have_content('Access Denied')
+ end
+ end
+ end
+
context 'without failures' do
before do
failed_build.update!(status: :success)
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 705ba78a0b7..9c165b17704 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -125,7 +125,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
- find('.js-primary-button').click
+ find('.js-modal-primary-action').click
wait_for_requests
end
@@ -156,7 +156,6 @@ describe 'Pipelines', :js do
context 'when retrying' do
before do
find('.js-pipelines-retry-button').click
- find('.js-primary-button').click
wait_for_requests
end
@@ -256,7 +255,7 @@ describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
- find('.js-primary-button').click
+ find('.js-modal-primary-action').click
end
it 'indicates that pipeline was canceled' do
@@ -388,9 +387,9 @@ describe 'Pipelines', :js do
it 'should be possible to cancel pending build' do
find('.js-builds-dropdown-button').click
- find('a.js-ci-action-icon').click
+ find('.js-ci-action').click
+ wait_for_requests
- expect(page).to have_content('canceled')
expect(build.reload).to be_canceled
end
end
@@ -407,7 +406,7 @@ describe 'Pipelines', :js do
within('.js-builds-dropdown-list') do
build_element = page.find('.mini-pipeline-graph-dropdown-item')
- expect(build_element['data-title']).to eq('build - failed <br> (unknown failure)')
+ expect(build_element['data-original-title']).to eq('build - failed <br> (unknown failure)')
end
end
end
@@ -517,16 +516,31 @@ describe 'Pipelines', :js do
end
it 'creates a new pipeline' do
- expect { click_on 'Run pipeline' }
+ expect { click_on 'Create pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last).to be_web
end
+
+ context 'when variables are specified' do
+ it 'creates a new pipeline with variables' do
+ page.within '.ci-variable-row-body' do
+ fill_in "Input variable key", with: "key_name"
+ fill_in "Input variable value", with: "value"
+ end
+
+ expect { click_on 'Create pipeline' }
+ .to change { Ci::Pipeline.count }.by(1)
+
+ expect(Ci::Pipeline.last.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq [{ key: "key_name", secret_value: "value" }.with_indifferent_access]
+ end
+ end
end
context 'without gitlab-ci.yml' do
before do
- click_on 'Run pipeline'
+ click_on 'Create pipeline'
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
@@ -539,7 +553,7 @@ describe 'Pipelines', :js do
click_link 'master'
end
- expect { click_on 'Run pipeline' }
+ expect { click_on 'Create pipeline' }
.to change { Ci::Pipeline.count }.by(1)
end
end
@@ -557,7 +571,7 @@ describe 'Pipelines', :js do
it 'has field to add a new pipeline' do
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
- expect(page).to have_content('Run on')
+ expect(page).to have_content('Create for')
end
end
diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb
new file mode 100644
index 00000000000..81a6b613cc8
--- /dev/null
+++ b/spec/features/projects/remote_mirror_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'Project remote mirror', :feature do
+ let(:project) { create(:project, :repository, :remote_mirror) }
+ let(:remote_mirror) { project.remote_mirrors.first }
+ let(:user) { create(:user) }
+
+ describe 'On a project', :js do
+ before do
+ project.add_master(user)
+ sign_in user
+ end
+
+ context 'when last_error is present but last_update_at is not' do
+ it 'renders error message without timstamp' do
+ remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: nil)
+
+ visit project_mirror_path(project)
+
+ expect(page).to have_content('The remote repository failed to update.')
+ end
+ end
+
+ context 'when last_error and last_update_at are present' do
+ it 'renders error message with timestamp' do
+ remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: Time.now - 5.minutes)
+
+ visit project_mirror_path(project)
+
+ expect(page).to have_content('The remote repository failed to update 5 minutes ago.')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/settings/lfs_settings_spec.rb b/spec/features/projects/settings/lfs_settings_spec.rb
index 0fd28a5681c..342be1d2a9d 100644
--- a/spec/features/projects/settings/lfs_settings_spec.rb
+++ b/spec/features/projects/settings/lfs_settings_spec.rb
@@ -1,21 +1,27 @@
require 'rails_helper'
describe 'Projects > Settings > LFS settings' do
- let(:admin) { create(:admin) }
let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:role) { :master }
context 'LFS enabled setting' do
before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- sign_in(admin)
+ sign_in(user)
+ project.add_role(user, role)
end
- it 'displays the correct elements', :js do
- visit edit_project_path(project)
+ context 'for master' do
+ let(:role) { :master }
- expect(page).to have_content('Git Large File Storage')
- expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true)
+ it 'displays the correct elements', :js do
+ visit edit_project_path(project)
+
+ expect(page).to have_content('Git Large File Storage')
+ expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true)
+ end
end
end
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index d9020333f28..cfdae246c09 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -8,6 +8,7 @@ describe "Projects > Settings > Pipelines settings" do
before do
sign_in(user)
project.add_role(user, role)
+ create(:project_auto_devops, project: project)
end
context 'for developer' do
@@ -27,10 +28,17 @@ describe "Projects > Settings > Pipelines settings" do
visit project_settings_ci_cd_path(project)
fill_in('Test coverage parsing', with: 'coverage_regex')
- click_on 'Save changes'
+
+ page.within '#js-general-pipeline-settings' do
+ click_on 'Save changes'
+ end
expect(page.status_code).to eq(200)
- expect(page).to have_button('Save changes', disabled: false)
+
+ page.within '#js-general-pipeline-settings' do
+ expect(page).to have_button('Save changes', disabled: false)
+ end
+
expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
end
@@ -38,10 +46,15 @@ describe "Projects > Settings > Pipelines settings" do
visit project_settings_ci_cd_path(project)
page.check('Auto-cancel redundant, pending pipelines')
- click_on 'Save changes'
+ page.within '#js-general-pipeline-settings' do
+ click_on 'Save changes'
+ end
expect(page.status_code).to eq(200)
- expect(page).to have_button('Save changes', disabled: false)
+
+ page.within '#js-general-pipeline-settings' do
+ expect(page).to have_button('Save changes', disabled: false)
+ end
checkbox = find_field('project_auto_cancel_pending_pipelines')
expect(checkbox).to be_checked
@@ -51,13 +64,39 @@ describe "Projects > Settings > Pipelines settings" do
it 'update auto devops settings' do
visit project_settings_ci_cd_path(project)
- fill_in('project_auto_devops_attributes_domain', with: 'test.com')
- page.choose('project_auto_devops_attributes_enabled_false')
- click_on 'Save changes'
+ page.within '#autodevops-settings' do
+ fill_in('project_auto_devops_attributes_domain', with: 'test.com')
+ page.choose('project_auto_devops_attributes_enabled_false')
+ click_on 'Save changes'
+ end
expect(page.status_code).to eq(200)
expect(project.auto_devops).to be_present
expect(project.auto_devops).not_to be_enabled
+ expect(project.auto_devops.domain).to eq('test.com')
+ end
+
+ context 'when there is a cluster with ingress and external_ip' do
+ before do
+ cluster = create(:cluster, projects: [project])
+ cluster.create_application_ingress!(external_ip: '192.168.1.100')
+ end
+
+ it 'shows the help text with the nip.io domain as an alternative to custom domain' do
+ visit project_settings_ci_cd_path(project)
+ expect(page).to have_content('192.168.1.100.nip.io can be used as an alternative to a custom domain')
+ end
+ end
+
+ context 'when there is no ingress' do
+ before do
+ create(:cluster, projects: [project])
+ end
+
+ it 'alternative to custom domain is not shown' do
+ visit project_settings_ci_cd_path(project)
+ expect(page).not_to have_content('can be used as an alternative to a custom domain')
+ end
end
end
end
diff --git a/spec/features/projects/settings/project_badges_spec.rb b/spec/features/projects/settings/project_badges_spec.rb
index cc3551a4c21..4893bef8884 100644
--- a/spec/features/projects/settings/project_badges_spec.rb
+++ b/spec/features/projects/settings/project_badges_spec.rb
@@ -22,7 +22,7 @@ feature 'Project Badges' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[0]).to have_content group_badge.link_url
expect(rows[1]).to have_content project_badge.link_url
@@ -49,7 +49,7 @@ feature 'Project Badges' do
click_button 'Add badge'
wait_for_requests
- within '.panel-body' do
+ within '.card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@@ -61,7 +61,7 @@ feature 'Project Badges' do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
@@ -75,7 +75,7 @@ feature 'Project Badges' do
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
within 'form' do
@@ -86,7 +86,7 @@ feature 'Project Badges' do
wait_for_requests
end
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@@ -100,7 +100,7 @@ feature 'Project Badges' do
it 'shows a modal when deleting a badge' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@@ -110,14 +110,14 @@ feature 'Project Badges' do
it 'deletes a badge when confirming the modal' do
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
- rows = all('.panel-body > div')
+ rows = all('.card-body > div')
expect(rows.length).to eq 1
expect(rows[0]).to have_content group_badge.link_url
end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index e1dfe617691..08b40653764 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- find('li', text: private_deploy_key.title).click_link('Edit')
+ find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
fill_in 'deploy_key_title', with: 'updated_deploy_key'
check 'deploy_key_deploy_keys_projects_attributes_0_can_push'
@@ -71,11 +71,15 @@ describe 'Projects > Settings > Repository settings' do
visit project_settings_repository_path(project)
- find('li', text: private_deploy_key.title).click_link('Edit')
+ find('.js-deployKeys-tab-available_project_keys').click()
+
+ find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
fill_in 'deploy_key_title', with: 'updated_deploy_key'
click_button 'Save changes'
+ find('.js-deployKeys-tab-available_project_keys').click()
+
expect(page).to have_content('updated_deploy_key')
end
@@ -83,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
- accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') }
+ accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click() }
expect(page).not_to have_content(private_deploy_key.title)
end
@@ -115,5 +119,20 @@ describe 'Projects > Settings > Repository settings' do
expect(page).to have_content('Your new project deploy token has been created')
end
end
+
+ context 'remote mirror settings' do
+ let(:user2) { create(:user) }
+
+ before do
+ project.add_master(user2)
+
+ visit project_settings_repository_path(project)
+ end
+
+ it 'shows push mirror settings' do
+ expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled')
+ expect(page).to have_selector('#project_remote_mirrors_attributes_0_url')
+ end
+ end
end
end
diff --git a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
new file mode 100644
index 00000000000..71a077039b7
--- /dev/null
+++ b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
@@ -0,0 +1,125 @@
+require "spec_helper"
+
+describe "User interacts with deploy keys", :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.owner }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples "attaches a key" do
+ it "attaches key" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ find(".badge", text: "1").click
+
+ click_button("Enable")
+
+ expect(page).not_to have_selector(".fa-spinner")
+ expect(current_path).to eq(project_settings_repository_path(project))
+
+ find(".js-deployKeys-tab-enabled_keys").click
+
+ expect(page).to have_content(deploy_key.title)
+ end
+ end
+ end
+
+ context "viewing deploy keys" do
+ let(:deploy_key) { create(:deploy_key) }
+
+ context "when project has keys" do
+ before do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ it "shows deploy keys" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ expect(page).to have_content(deploy_key.title)
+ end
+ end
+ end
+
+ context "when another project has keys" do
+ let(:another_project) { create(:project) }
+
+ before do
+ create(:deploy_keys_project, project: another_project, deploy_key: deploy_key)
+
+ another_project.add_master(user)
+ end
+
+ it "shows deploy keys" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ find('.js-deployKeys-tab-available_project_keys').click
+
+ expect(page).to have_content(deploy_key.title)
+ expect(find(".js-deployKeys-tab-available_project_keys .badge")).to have_content("1")
+ end
+ end
+ end
+
+ context "when there are public deploy keys" do
+ let!(:deploy_key) { create(:deploy_key, public: true) }
+
+ it "shows public deploy keys" do
+ visit(project_deploy_keys_path(project))
+
+ page.within(".deploy-keys") do
+ find(".js-deployKeys-tab-public_keys").click
+
+ expect(page).to have_content(deploy_key.title)
+ end
+ end
+ end
+ end
+
+ context "adding deploy keys" do
+ before do
+ visit(project_deploy_keys_path(project))
+ end
+
+ it "adds new key" do
+ DEPLOY_KEY_TITLE = attributes_for(:key)[:title]
+ DEPLOY_KEY_BODY = attributes_for(:key)[:key]
+
+ fill_in("deploy_key_title", with: DEPLOY_KEY_TITLE)
+ fill_in("deploy_key_key", with: DEPLOY_KEY_BODY)
+
+ click_button("Add key")
+
+ expect(current_path).to eq(project_settings_repository_path(project))
+
+ page.within(".deploy-keys") do
+ expect(page).to have_content(DEPLOY_KEY_TITLE)
+ end
+ end
+ end
+
+ context "attaching existing keys" do
+ context "from another project" do
+ let(:another_project) { create(:project) }
+ let(:deploy_key) { create(:deploy_key) }
+
+ before do
+ create(:deploy_keys_project, project: another_project, deploy_key: deploy_key)
+
+ another_project.add_master(user)
+ end
+
+ it_behaves_like "attaches a key"
+ end
+
+ context "when keys are public" do
+ let!(:deploy_key) { create(:deploy_key, public: true) }
+
+ it_behaves_like "attaches a key"
+ end
+ end
+end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index a906fa20233..e44361fbe26 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -65,7 +65,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Auto DevOps button' do
it '"Enable Auto DevOps" button linked to settings page' do
page.within('.project-stats') do
- expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -75,7 +75,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
page.within('.project-stats') do
- expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
end
@@ -212,7 +212,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Auto DevOps button' do
it '"Enable Auto DevOps" button linked to settings page' do
page.within('.project-stats') do
- expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
@@ -222,7 +222,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
page.within('.project-stats') do
- expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
+ expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index d96c7e655ba..3017048e506 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -44,10 +44,17 @@ feature 'Multi-file editor new directory', :js do
wait_for_requests
+ find('.js-ide-commit-mode').click
+
+ find('.multi-file-commit-list-item').hover
+ first('.multi-file-discard-btn .btn').click
+
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
+ find('.js-ide-edit-mode').click
+
expect(page).to have_content('folder name')
end
end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index a4cbd5cf766..56471c8e7aa 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -34,6 +34,11 @@ feature 'Multi-file editor new file', :js do
wait_for_requests
+ find('.js-ide-commit-mode').click
+
+ find('.multi-file-commit-list-item').hover
+ first('.multi-file-discard-btn .btn').click
+
fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index 8e53ae15700..4dfc325b37e 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -35,17 +35,4 @@ feature 'Multi-file editor upload file', :js do
expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
end
-
- it 'uploads image file' do
- find('.add-to-tree').click
-
- # make the field visible so capybara can use it
- execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
- attach_file('file-upload', img_file)
-
- find('.add-to-tree').click
-
- expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
- expect(page).not_to have_selector('.monaco-editor')
- end
end
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index cf80517b934..8a9255db9e8 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -37,7 +37,7 @@ describe 'Projects > User sees sidebar' do
visit project_path(project)
within('.nav-sidebar') do
- expect(page).to have_content 'Overview'
+ expect(page).to have_content 'Project'
expect(page).to have_content 'Issues'
expect(page).to have_content 'Wiki'
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 47c5a8161d9..495a010b32c 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -13,6 +13,8 @@ describe 'User uses shortcuts', :js do
context 'when navigating to the Project pages' do
it 'redirects to the details page' do
+ visit project_issues_path(project)
+
find('body').native.send_key('g')
find('body').native.send_key('p')
@@ -22,7 +24,7 @@ describe 'User uses shortcuts', :js do
it 'redirects to the activity page' do
find('body').native.send_key('g')
- find('body').native.send_key('e')
+ find('body').native.send_key('v')
expect(page).to have_active_navigation('Project')
expect(page).to have_active_sub_navigation('Activity')
@@ -72,10 +74,19 @@ describe 'User uses shortcuts', :js do
expect(page).to have_active_sub_navigation('List')
end
+ it 'redirects to the issue board page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('b')
+
+ expect(page).to have_active_navigation('Issues')
+ expect(page).to have_active_sub_navigation('Board')
+ end
+
it 'redirects to the new issue page' do
find('body').native.send_key('i')
expect(page).to have_content(project.title)
+ expect(page).to have_content('New Issue')
end
end
@@ -88,6 +99,34 @@ describe 'User uses shortcuts', :js do
end
end
+ context 'when navigating to the CI / CD pages' do
+ it 'redirects to the Jobs page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('j')
+
+ expect(page).to have_active_navigation('CI / CD')
+ expect(page).to have_active_sub_navigation('Jobs')
+ end
+ end
+
+ context 'when navigating to the Operations pages' do
+ it 'redirects to the Environments page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('e')
+
+ expect(page).to have_active_navigation('Operations')
+ expect(page).to have_active_sub_navigation('Environments')
+ end
+
+ it 'redirects to the Kubernetes page' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('k')
+
+ expect(page).to have_active_navigation('Operations')
+ expect(page).to have_active_sub_navigation('Kubernetes')
+ end
+ end
+
context 'when navigating to the Snippets pages' do
it 'redirects to the snippets page' do
find('body').native.send_key('g')
diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb
new file mode 100644
index 00000000000..7b982301ffc
--- /dev/null
+++ b/spec/features/projects/user_views_empty_project_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe 'User views an empty project' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:user) { create(:user) }
+
+ shared_examples 'allowing push to default branch' do
+ before do
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'shows push-to-master instructions' do
+ expect(page).to have_content('git push -u origin master')
+ end
+ end
+
+ describe 'as a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'allowing push to default branch'
+ end
+
+ describe 'as an admin' do
+ let(:user) { create(:user, :admin) }
+
+ it_behaves_like 'allowing push to default branch'
+ end
+
+ describe 'as a developer' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit project_path(project)
+ end
+
+ it 'does not show push-to-master instructions' do
+ expect(page).not_to have_content('git push -u origin master')
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 006c15d60c5..e473739a6aa 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Projects > Wiki > User previews markdown changes', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_content) do
<<-HEREDOC
[regular link](regular)
@@ -155,4 +155,27 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
end
end
end
+
+ it "does not linkify double brackets inside code blocks as expected" do
+ click_link 'New page'
+ page.within '#modal-new-wiki' do
+ fill_in :new_wiki_path, with: 'linkify_test'
+ click_button 'Create page'
+ end
+
+ page.within '.wiki-form' do
+ fill_in :wiki_content, with: <<-HEREDOC
+ `[[do_not_linkify]]`
+ ```
+ [[also_do_not_linkify]]
+ ```
+ HEREDOC
+ click_on "Preview"
+ end
+
+ expect(page).to have_content("do_not_linkify")
+
+ expect(page.html).to include('[[do_not_linkify]]')
+ expect(page.html).to include('[[also_do_not_linkify]]')
+ end
end
diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb
index f70d1e710dd..6178361082e 100644
--- a/spec/features/projects/wiki/shortcuts_spec.rb
+++ b/spec/features/projects/wiki/shortcuts_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Wiki shortcuts', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) }
before do
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 4a9d1cb87e1..9989e1ffda7 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -1,6 +1,6 @@
-require 'spec_helper'
+require "spec_helper"
-describe 'User creates wiki page' do
+describe "User creates wiki page" do
let(:user) { create(:user) }
before do
@@ -10,67 +10,104 @@ describe 'User creates wiki page' do
visit(project_wikis_path(project))
end
- context 'when wiki is empty' do
- context 'in a user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ context "when wiki is empty" do
+ context "in a user namespace" do
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- it 'shows validation error message' do
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: '')
- click_on('Create page')
+ it "shows validation error message" do
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "")
+
+ click_on("Create page")
end
- expect(page).to have_content('The form contains the following error:')
- expect(page).to have_content("Content can't be blank")
+ expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "[link test](test)")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: '[link test](test)')
- click_on('Create page')
+ click_on("Create page")
end
- expect(page).to have_content('Home')
- expect(page).to have_content('link test')
+ expect(page).to have_content("Home").and have_content("link test")
- click_link('link test')
+ click_link("link test")
- expect(page).to have_content('Create Page')
+ expect(page).to have_content("Create Page")
end
- it 'shows non-escaped link in the pages list', :js do
- click_link('New page')
+ it "shows non-escaped link in the pages list", :js do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'one/two/three-test')
- click_on('Create page')
+ 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')
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "wiki content")
+
+ click_on("Create page")
end
- expect(current_path).to include('one/two/three-test')
+ expect(current_path).to include("one/two/three-test")
expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
end
- it 'has "Create home" as a commit message' do
- expect(page).to have_field('wiki[message]', with: 'Create home')
+ it "has `Create home` as a commit message" do
+ expect(page).to have_field("wiki[message]", with: "Create home")
end
- it 'creates a page from the home page' do
- fill_in(:wiki_content, with: 'My awesome wiki!')
+ it "creates a page from the home page" do
+ fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n")
+ fill_in(:wiki_message, with: "Adding links to wiki")
+
+ page.within(".wiki-form") do
+ click_button("Create page")
+ end
+
+ expect(current_path).to eq(project_wiki_path(project, "home"))
+ expect(page).to have_content("test GitLab API doc Rake tasks Wiki header")
+ .and have_content("Home")
+ .and have_content("Last edited by #{user.name}")
+ .and have_header_with_correct_id_and_link(1, "Wiki header", "wiki-header")
+
+ click_link("test")
- page.within('.wiki-form') do
- click_button('Create page')
+ expect(current_path).to eq(project_wiki_path(project, "test"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Test").and have_content("Create Page")
+ end
+
+ click_link("Home")
+
+ expect(current_path).to eq(project_wiki_path(project, "home"))
+
+ click_link("GitLab API")
+
+ expect(current_path).to eq(project_wiki_path(project, "api"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Create").and have_content("Api")
end
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ click_link("Home")
+
+ expect(current_path).to eq(project_wiki_path(project, "home"))
+
+ click_link("Rake tasks")
+
+ expect(current_path).to eq(project_wiki_path(project, "raketasks"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Create").and have_content("Rake")
+ end
end
- it 'creates ASCII wiki with LaTeX blocks', :js do
- stub_application_setting(plantuml_url: 'http://localhost', plantuml_enabled: true)
+ it "creates ASCII wiki with LaTeX blocks", :js do
+ stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true)
ascii_content = <<~MD
:stem: latexmath
@@ -90,153 +127,164 @@ describe 'User creates wiki page' do
stem:[2+2] is 4
MD
- find('#wiki_format option[value=asciidoc]').select_option
+ find("#wiki_format option[value=asciidoc]").select_option
+
fill_in(:wiki_content, with: ascii_content)
- page.within('.wiki-form') do
- click_button('Create page')
+ page.within(".wiki-form") do
+ click_button("Create page")
end
- page.within '.wiki' do
- expect(page).to have_selector('.katex', count: 3)
- expect(page).to have_content('2+2 is 4')
+ page.within ".wiki" do
+ expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
end
end
end
- context 'in a group namespace', :js do
- let(:project) { create(:project, namespace: create(:group, :public)) }
+ context "in a group namespace", :js do
+ let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
- it 'has "Create home" as a commit message' do
- expect(page).to have_field('wiki[message]', with: 'Create home')
+ it "has `Create home` as a commit message" do
+ expect(page).to have_field("wiki[message]", with: "Create home")
end
- it 'creates a page from from the home page' do
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ it "creates a page from from the home page" do
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
+
+ click_button("Create page")
end
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Home")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
end
end
- context 'when wiki is not empty', :js do
+ context "when wiki is not empty", :js do
before do
- create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: 'home', content: 'Home page' })
+ create(:wiki_page, wiki: create(:project, :wiki_repo, namespace: user.namespace).wiki, attrs: { title: "home", content: "Home page" })
end
- context 'in a user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ context "in a user namespace" do
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- context 'via the "new wiki page" page' do
- it 'creates a page with a single word' do
- click_link('New page')
+ context "via the `new wiki page` page" do
+ it "creates a page with a single word" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'foo')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "foo")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create foo')
+ expect(page).to have_field("wiki[message]", with: "Create foo")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Foo')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Foo")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
- it 'creates a page with spaces in the name' do
- click_link('New page')
+ it "creates a page with spaces in the name" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'Spaces in the name')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "Spaces in the name")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create spaces in the name')
+ expect(page).to have_field("wiki[message]", with: "Create spaces in the name")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Spaces in the name')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Spaces in the name")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
- it 'creates a page with hyphens in the name' do
- click_link('New page')
+ it "creates a page with hyphens in the name" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'hyphens-in-the-name')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "hyphens-in-the-name")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create hyphens in the name')
+ expect(page).to have_field("wiki[message]", with: "Create hyphens in the name")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Hyphens in the name')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Hyphens in the name")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
end
- it 'shows the autocompletion dropdown' do
- click_link('New page')
+ it "shows the autocompletion dropdown" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'test-autocomplete')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "test-autocomplete")
+
+ click_button("Create page")
end
- page.within('.wiki-form') do
- find('#wiki_content').native.send_keys('')
- fill_in(:wiki_content, with: '@')
+ page.within(".wiki-form") do
+ find("#wiki_content").native.send_keys("")
+
+ fill_in(:wiki_content, with: "@")
end
- expect(page).to have_selector('.atwho-view')
+ expect(page).to have_selector(".atwho-view")
end
end
- context 'in a group namespace' do
- let(:project) { create(:project, namespace: create(:group, :public)) }
+ context "in a group namespace" do
+ let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
- context 'via the "new wiki page" page' do
- it 'creates a page' do
- click_link('New page')
+ context "via the `new wiki page` page" do
+ it "creates a page" do
+ click_link("New page")
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'foo')
- click_button('Create page')
+ page.within("#modal-new-wiki") do
+ fill_in(:new_wiki_path, with: "foo")
+
+ click_button("Create page")
end
# Commit message field should have correct value.
- expect(page).to have_field('wiki[message]', with: 'Create foo')
+ expect(page).to have_field("wiki[message]", with: "Create foo")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
- page.within('.wiki-form') do
- fill_in(:wiki_content, with: 'My awesome wiki!')
- click_button('Create page')
+ click_button("Create page")
end
- expect(page).to have_content('Foo')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ expect(page).to have_content("Foo")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
end
end
end
diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
index 605e332196b..2c67cec6b67 100644
--- a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
-feature 'User deletes wiki page' do
+feature 'User deletes wiki page', :js do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
before do
@@ -13,6 +13,7 @@ feature 'User deletes wiki page' do
it 'deletes a page' do
click_on('Edit')
click_on('Delete')
+ find('.js-modal-primary-action').click
expect(page).to have_content('Page was successfully deleted')
end
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
index 37a118c34ab..823399ac3c3 100644
--- a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe 'Projects > Wiki > User views Git access wiki page' do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :wiki_repo, :public) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
before do
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 ef1bb712846..e019e3ce5a5 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -14,7 +14,7 @@ describe 'User updates wiki page' do
end
context 'in a user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it 'redirects back to the home edit page' do
page.within(:css, '.wiki-form .form-actions') do
@@ -66,7 +66,7 @@ describe 'User updates wiki page' do
end
context 'in a user namespace' do
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
it 'updates a page' do
click_link('Edit')
@@ -134,7 +134,7 @@ describe 'User updates wiki page' do
end
context 'in a group namespace' do
- let(:project) { create(:project, namespace: create(:group, :public)) }
+ let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it 'updates a page' do
click_link('Edit')
@@ -154,7 +154,7 @@ describe 'User updates wiki page' do
end
context 'when the page is in a subdir' do
- let!(:project) { create(:project, namespace: user.namespace) }
+ 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}" }
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index 2682b62fa04..92b50169476 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -11,6 +11,7 @@ describe 'Projects > Wiki > User views wiki in project page' do
context 'when repository is disabled for project' do
let(:project) do
create(:project,
+ :wiki_repo,
:repository_disabled,
:merge_requests_disabled,
:builds_disabled)
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 306e382119a..6661714222a 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'User views a wiki page' do
shared_examples 'wiki page user view' do
let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) do
create(:wiki_page,
wiki: project.wiki,
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 43cabd3b9f2..0c28a853b54 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -68,8 +68,10 @@ feature 'Protected Branches', :js do
within form do
find(".js-allowed-to-merge").click
+ wait_for_requests
click_link 'No one'
find(".js-allowed-to-push").click
+ wait_for_requests
click_link 'Developers + Masters'
end
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index 74890c86047..a9e815eaf4f 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'RavenJS' do
- let(:raven_path) { '/raven.bundle.js' }
+ let(:raven_path) { '/raven.chunk.js' }
it 'should not load raven if sentry is disabled' do
visit new_user_session_path
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index df65c2d2f83..e0cd963fe39 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -15,7 +15,7 @@ feature 'Runners' do
end
scenario 'user can see a button to install runners on kubernetes clusters' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page).to have_link('Install Runner on Kubernetes', href: project_clusters_path(project))
end
@@ -36,7 +36,7 @@ feature 'Runners' do
end
scenario 'user sees the specific runner' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
expect(page).to have_content(specific_runner.display_name)
@@ -48,7 +48,7 @@ feature 'Runners' do
end
scenario 'user can pause and resume the specific runner' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
expect(page).to have_content('Pause')
@@ -68,7 +68,7 @@ feature 'Runners' do
end
scenario 'user removes an activated specific runner if this is last project for that runners' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
click_on 'Remove Runner'
@@ -78,7 +78,7 @@ feature 'Runners' do
end
scenario 'user edits the runner to be protected' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
first('.edit-runner > a').click
@@ -98,7 +98,7 @@ feature 'Runners' do
end
scenario 'user edits runner not to run untagged jobs' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.activated-specific-runners' do
first('.edit-runner > a').click
@@ -117,7 +117,7 @@ feature 'Runners' do
given!(:shared_runner) { create(:ci_runner, :shared) }
scenario 'user sees CI/CD setting page' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
end
@@ -134,7 +134,7 @@ feature 'Runners' do
end
scenario 'user enables and disables a specific runner' do
- visit runners_path(project)
+ visit project_runners_path(project)
within '.available-specific-runners' do
click_on 'Enable for this project'
@@ -159,7 +159,7 @@ feature 'Runners' do
end
scenario 'user sees shared runners description' do
- visit runners_path(project)
+ visit project_runners_path(project)
expect(page.find('.shared-runners-description')).to have_content(shared_runners_html)
end
@@ -174,11 +174,185 @@ feature 'Runners' do
end
scenario 'user enables shared runners' do
- visit runners_path(project)
+ visit project_runners_path(project)
click_on 'Enable shared Runners'
expect(page.find('.shared-runners-description')).to have_content('Disable shared Runners')
end
end
+
+ context 'group runners in project settings' do
+ background do
+ project.add_master(user)
+ end
+
+ given(:group) { create :group }
+
+ context 'as project and group master' do
+ background do
+ group.add_master(user)
+ end
+
+ context 'project with a group but no group runner' do
+ given(:project) { create :project, group: group }
+
+ scenario 'group runners are not available' do
+ visit project_runners_path(project)
+
+ expect(page).to have_content 'This group does not provide any group Runners yet'
+
+ expect(page).to have_content 'Group masters can register group runners in the Group CI/CD settings'
+ expect(page).not_to have_content 'Ask your group master to setup a group Runner'
+ end
+ end
+ end
+
+ context 'as project master' do
+ context 'project without a group' do
+ given(:project) { create :project }
+
+ scenario 'group runners are not available' do
+ visit project_runners_path(project)
+
+ expect(page).to have_content 'This project does not belong to a group and can therefore not make use of group Runners.'
+ end
+ end
+
+ context 'project with a group but no group runner' do
+ given(:group) { create :group }
+ given(:project) { create :project, group: group }
+
+ scenario 'group runners are not available' do
+ visit project_runners_path(project)
+
+ expect(page).to have_content 'This group does not provide any group Runners yet.'
+
+ expect(page).not_to have_content 'Group masters can register group runners in the Group CI/CD settings'
+ expect(page).to have_content 'Ask your group master to setup a group Runner.'
+ end
+ end
+
+ context 'project with a group and a group runner' do
+ given(:group) { create :group }
+ given(:project) { create :project, group: group }
+ given!(:ci_runner) { create :ci_runner, groups: [group], description: 'group-runner' }
+
+ scenario 'group runners are available' do
+ visit project_runners_path(project)
+
+ expect(page).to have_content 'Available group Runners : 1'
+ expect(page).to have_content 'group-runner'
+ end
+
+ scenario 'group runners may be disabled for a project' do
+ visit project_runners_path(project)
+
+ click_on 'Disable group Runners'
+
+ expect(page).to have_content 'Enable group Runners'
+ expect(project.reload.group_runners_enabled).to be false
+
+ click_on 'Enable group Runners'
+
+ expect(page).to have_content 'Disable group Runners'
+ expect(project.reload.group_runners_enabled).to be true
+ end
+ end
+ end
+ end
+
+ context 'group runners in group settings' do
+ given(:group) { create :group }
+ background do
+ group.add_master(user)
+ end
+
+ context 'group with no runners' do
+ scenario 'there are no runners displayed' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).to have_content 'This group does not provide any group Runners yet'
+ end
+ end
+
+ context 'group with a runner' do
+ let!(:runner) { create :ci_runner, groups: [group], description: 'group-runner' }
+
+ scenario 'the runner is visible' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).not_to have_content 'This group does not provide any group Runners yet'
+ expect(page).to have_content 'Available group Runners : 1'
+ expect(page).to have_content 'group-runner'
+ end
+
+ scenario 'user can pause and resume the group runner' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).to have_content('Pause')
+ expect(page).not_to have_content('Resume')
+
+ click_on 'Pause'
+
+ expect(page).not_to have_content('Pause')
+ expect(page).to have_content('Resume')
+
+ click_on 'Resume'
+
+ expect(page).to have_content('Pause')
+ expect(page).not_to have_content('Resume')
+ end
+
+ scenario 'user can view runner details' do
+ visit group_settings_ci_cd_path(group)
+
+ expect(page).to have_content(runner.display_name)
+
+ click_on runner.short_sha
+
+ expect(page).to have_content(runner.platform)
+ end
+
+ scenario 'user can remove a group runner' do
+ visit group_settings_ci_cd_path(group)
+
+ click_on 'Remove Runner'
+
+ expect(page).not_to have_content(runner.display_name)
+ end
+
+ scenario 'user edits the runner to be protected' do
+ visit group_settings_ci_cd_path(group)
+
+ first('.edit-runner > a').click
+
+ expect(page.find_field('runner[access_level]')).not_to be_checked
+
+ check 'runner_access_level'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Protected Yes'
+ end
+
+ context 'when a runner has a tag' do
+ background do
+ runner.update(tag_list: ['tag'])
+ end
+
+ scenario 'user edits runner not to run untagged jobs' do
+ visit group_settings_ci_cd_path(group)
+
+ first('.edit-runner > a').click
+
+ expect(page.find_field('runner[run_untagged]')).to be_checked
+
+ uncheck 'runner_run_untagged'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Can run untagged jobs No'
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 7934779058f..5098fb49ee1 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, namespace: user.namespace) }
+ let(:project) { create(:project, :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/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 6088b831c14..db141ef7096 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -96,10 +96,11 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed commit by bette cartwright')) do
click_on 'Unverified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with an unverified signature.'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with an unverified signature.'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
@@ -110,12 +111,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do
click_on 'Unverified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.'
- expect(page).to have_content 'Bette Cartwright'
- expect(page).to have_content '@bette.cartwright'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.'
+ expect(page).to have_content 'Bette Cartwright'
+ expect(page).to have_content '@bette.cartwright'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
@@ -126,12 +128,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed commit by bette cartwright')) do
click_on 'Unverified'
- within '.popover' do
- expect(page).to have_content "This commit was signed with a different user's verified signature."
- expect(page).to have_content 'Bette Cartwright'
- expect(page).to have_content '@bette.cartwright'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content "This commit was signed with a different user's verified signature."
+ expect(page).to have_content 'Bette Cartwright'
+ expect(page).to have_content '@bette.cartwright'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
end
end
@@ -142,12 +145,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
click_on 'Verified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
- expect(page).to have_content 'Nannie Bernhard'
- expect(page).to have_content '@nannie.bernhard'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
+ expect(page).to have_content 'Nannie Bernhard'
+ expect(page).to have_content '@nannie.bernhard'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
@@ -167,12 +171,13 @@ describe 'GPG signed commits', :js do
within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
click_on 'Verified'
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
- expect(page).to have_content 'Nannie Bernhard'
- expect(page).to have_content 'nannie.bernhard@example.com'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
- end
+ end
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
+ expect(page).to have_content 'Nannie Bernhard'
+ expect(page).to have_content 'nannie.bernhard@example.com'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
end
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 19784120108..6be2606fd0d 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -23,7 +23,7 @@ feature 'Triggers', :js do
click_button 'Add trigger'
# See if input has error due to empty value
- expect(page.find('form.gl-show-field-errors .gl-field-error')['style']).to eq 'display: block;'
+ expect(page.find('form.gl-show-field-errors .gl-field-error')).to be_visible
end
scenario 'adds new trigger with description' do
diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb
new file mode 100644
index 00000000000..631d7e3bced
--- /dev/null
+++ b/spec/features/users/active_sessions_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+feature 'Active user sessions', :clean_gitlab_redis_shared_state do
+ scenario 'Successful login adds a new active user login' do
+ now = Time.zone.parse('2018-03-12 09:06')
+ Timecop.freeze(now) do
+ user = create(:user)
+ gitlab_sign_in(user)
+ expect(current_path).to eq root_path
+
+ sessions = ActiveSession.list(user)
+ expect(sessions.count).to eq 1
+
+ # refresh the current page updates the updated_at
+ Timecop.freeze(now + 1.minute) do
+ visit current_path
+
+ sessions = ActiveSession.list(user)
+ expect(sessions.first).to have_attributes(
+ created_at: Time.zone.parse('2018-03-12 09:06'),
+ updated_at: Time.zone.parse('2018-03-12 09:07')
+ )
+ end
+ end
+ end
+
+ scenario 'Successful login cleans up obsolete entries' do
+ user = create(:user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ end
+
+ gitlab_sign_in(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d'
+ end
+ end
+
+ scenario 'Sessionless login does not clean up obsolete entries' do
+ user = create(:user)
+ personal_access_token = create(:personal_access_token, user: user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ end
+
+ visit user_path(user, :atom, private_token: personal_access_token.token)
+ expect(page.status_code).to eq 200
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d'
+ end
+ end
+
+ scenario 'Logout deletes the active user login' do
+ user = create(:user)
+ gitlab_sign_in(user)
+ expect(current_path).to eq root_path
+
+ expect(ActiveSession.list(user).count).to eq 1
+
+ gitlab_sign_out
+ expect(current_path).to eq new_user_session_path
+
+ expect(ActiveSession.list(user)).to be_empty
+ end
+end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 9e10bfb2adc..1f8d31a5c88 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Login' do
+ include TermsHelper
+
scenario 'Successful user signin invalidates password reset token' do
user = create(:user)
@@ -392,11 +394,150 @@ feature 'Login' do
end
def ensure_one_active_tab
- expect(page).to have_selector('ul.new-session-tabs > li.active', count: 1)
+ 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
+ let(:user) { create(:user) }
+
+ before do
+ enforce_terms
+ end
+
+ it 'asks to accept the terms on first login' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+
+ click_button 'Sign in'
+
+ expect_to_be_on_terms_page
+
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(root_path)
+ expect(page).not_to have_content('You are already signed in.')
+ end
+
+ it 'does not ask for terms when the user already accepted them' do
+ accept_terms(user)
+
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+
+ click_button 'Sign in'
+
+ expect(current_path).to eq(root_path)
+ end
+
+ context 'when 2FA is required for the user' do
+ before do
+ group = create(:group, require_two_factor_authentication: true)
+ group.add_developer(user)
+ end
+
+ context 'when the user did not enable 2FA' do
+ it 'asks to set 2FA before asking to accept the terms' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+
+ click_button 'Sign in'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(profile_two_factor_auth_path)
+
+ fill_in 'pin_code', with: user.reload.current_otp
+
+ click_button 'Register with two-factor app'
+ click_link 'Proceed'
+
+ expect(current_path).to eq(profile_account_path)
+ end
+ end
+
+ context 'when the user already enabled 2FA' do
+ before do
+ user.update!(otp_required_for_login: true,
+ otp_secret: User.generate_otp_secret(32))
+ end
+
+ it 'asks the user to accept the terms' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+ click_button 'Sign in'
+
+ fill_in 'user_otp_attempt', with: user.reload.current_otp
+ click_button 'Verify code'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(root_path)
+ end
+ end
+ end
+
+ context 'when the users password is expired' do
+ before do
+ user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
+ end
+
+ it 'asks the user to accept the terms before setting a new password' do
+ visit new_user_session_path
+
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
+ click_button 'Sign in'
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(new_profile_password_path)
+
+ fill_in 'user_current_password', with: '12345678'
+ fill_in 'user_password', with: 'new password'
+ fill_in 'user_password_confirmation', with: 'new password'
+ click_button 'Set new password'
+
+ expect(page).to have_content('Password successfully changed')
+ end
+ end
+
+ context 'when the user does not have an email configured' do
+ let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
+
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
+ end
+
+ it 'asks the user to accept the terms before setting an email' do
+ gitlab_sign_in_via('saml', user, 'my-uid')
+
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
+
+ expect(current_path).to eq(profile_path)
+
+ fill_in 'Email', with: 'hello@world.com'
+
+ click_button 'Update profile settings'
+
+ expect(page).to have_content('Profile was successfully updated')
+ end
+ end
+ end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 5d539f0ccbe..b5bd5c505f2 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Signup' do
+ include TermsHelper
+
let(:new_user) { build_stubbed(:user) }
describe 'username validation', :js do
@@ -132,4 +134,27 @@ describe 'Signup' do
expect(page.body).not_to match(/#{new_user.password}/)
end
end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ it 'asks the user to accept terms before going to the dashboard' do
+ visit root_path
+
+ fill_in 'new_user_name', with: new_user.name
+ fill_in 'new_user_username', with: new_user.username
+ fill_in 'new_user_email', with: new_user.email
+ fill_in 'new_user_email_confirmation', with: new_user.email
+ fill_in 'new_user_password', with: new_user.password
+ click_button "Register"
+
+ expect_to_be_on_terms_page
+
+ click_button 'Accept terms'
+
+ expect(current_path).to eq dashboard_projects_path
+ end
+ end
end
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
new file mode 100644
index 00000000000..f9469adbfe3
--- /dev/null
+++ b/spec/features/users/terms_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe 'Users > Terms' do
+ include TermsHelper
+
+ let(:user) { create(:user) }
+ let!(:term) { create(:term, terms: 'By accepting, you promise to be nice!') }
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ sign_in(user)
+ end
+
+ it 'shows the terms' do
+ visit terms_path
+
+ expect(page).to have_content('By accepting, you promise to be nice!')
+ end
+
+ context 'declining the terms' do
+ it 'returns the user to the app' do
+ visit terms_path
+
+ click_button 'Decline and sign out'
+
+ expect(page).not_to have_content(term.terms)
+ expect(user.reload.terms_accepted?).to be(false)
+ end
+ end
+
+ context 'accepting the terms' do
+ it 'returns the user to the app' do
+ visit terms_path
+
+ click_button 'Accept terms'
+
+ expect(page).not_to have_content(term.terms)
+ expect(user.reload.terms_accepted?).to be(true)
+ end
+ end
+
+ context 'terms were enforced while session is active', :js do
+ let(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'redirects to terms and back to where the user was going' do
+ visit project_path(project)
+
+ enforce_terms
+
+ within('.nav-sidebar') do
+ click_link 'Issues'
+ end
+
+ expect_to_be_on_terms_page
+
+ click_button('Accept terms')
+
+ expect(current_path).to eq(project_issues_path(project))
+ end
+
+ it 'redirects back to the page the user was trying to save' do
+ visit new_project_issue_path(project)
+
+ fill_in :issue_title, with: 'Hello world, a new issue'
+ fill_in :issue_description, with: "We don't want to lose what the user typed"
+
+ enforce_terms
+
+ click_button 'Submit issue'
+
+ expect(current_path).to eq(terms_path)
+
+ click_button('Accept terms')
+
+ expect(current_path).to eq(new_project_issue_path(project))
+ expect(find_field('issue_title').value).to eq('Hello world, a new issue')
+ expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed")
+ end
+ end
+
+ context 'when the terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'signing out', :js do
+ it 'allows the user to sign out without a response' do
+ visit terms_path
+
+ find('.header-user-dropdown-toggle').click
+ click_link('Sign out')
+
+ expect(page).to have_content('Sign in')
+ expect(page).to have_content('Register')
+ end
+ end
+ end
+end
diff --git a/spec/features/users/user_browses_projects_on_user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb
index a70637c8370..7bede0b0d48 100644
--- a/spec/features/users/user_browses_projects_on_user_page_spec.rb
+++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb
@@ -27,8 +27,8 @@ describe 'Users > User browses projects on user page', :js do
end
it 'paginates projects', :js do
- project = create(:project, namespace: user.namespace)
- project2 = create(:project, namespace: user.namespace)
+ project = create(:project, namespace: user.namespace, updated_at: 2.minutes.since)
+ project2 = create(:project, namespace: user.namespace, updated_at: 1.minute.since)
allow(Project).to receive(:default_per_page).and_return(1)
sign_in(user)
@@ -41,11 +41,11 @@ describe 'Users > User browses projects on user page', :js do
wait_for_requests
- expect(page).to have_content(project2.name)
+ expect(page).to have_content(project.name)
click_link('Next')
- expect(page).to have_content(project.name)
+ expect(page).to have_content(project2.name)
end
context 'when not signed in' do
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 375bcc9087e..796d40cb625 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -35,15 +35,6 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(project)
end
- it 'does not include projects shared with the group' do
- project = create(:project, namespace: group)
- other_project = create(:project)
- other_project.project_group_links.create(group: group,
- group_access: ProjectGroupLink::MASTER)
-
- expect(finder.execute).to contain_exactly(project)
- end
-
context 'when archived is `true`' do
let(:params) { { archived: 'true' } }
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
index abc470788e1..16c0d418d98 100644
--- a/spec/finders/groups_finder_spec.rb
+++ b/spec/finders/groups_finder_spec.rb
@@ -2,43 +2,71 @@ require 'spec_helper'
describe GroupsFinder do
describe '#execute' do
- let(:user) { create(:user) }
-
- context 'root level groups' do
- let!(:private_group) { create(:group, :private) }
- let!(:internal_group) { create(:group, :internal) }
- let!(:public_group) { create(:group, :public) }
-
- context 'without a user' do
- subject { described_class.new.execute }
-
- it { is_expected.to eq([public_group]) }
+ let(:user) { create(:user) }
+
+ describe 'root level groups' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:user_type, :params, :results) do
+ nil | { all_available: true } | %i(public_group user_public_group)
+ nil | { all_available: false } | %i(public_group user_public_group)
+ nil | {} | %i(public_group user_public_group)
+
+ :regular | { all_available: true } | %i(public_group internal_group user_public_group user_internal_group
+ user_private_group)
+ :regular | { all_available: false } | %i(user_public_group user_internal_group user_private_group)
+ :regular | {} | %i(public_group internal_group user_public_group user_internal_group user_private_group)
+
+ :external | { all_available: true } | %i(public_group user_public_group user_internal_group user_private_group)
+ :external | { all_available: false } | %i(user_public_group user_internal_group user_private_group)
+ :external | {} | %i(public_group user_public_group user_internal_group user_private_group)
+
+ :admin | { all_available: true } | %i(public_group internal_group private_group user_public_group
+ user_internal_group user_private_group)
+ :admin | { all_available: false } | %i(user_public_group user_internal_group user_private_group)
+ :admin | {} | %i(public_group internal_group private_group user_public_group user_internal_group
+ user_private_group)
end
- context 'with a user' do
- subject { described_class.new(user).execute }
-
- context 'normal user' do
- it { is_expected.to contain_exactly(public_group, internal_group) }
- end
-
- context 'external user' do
- let(:user) { create(:user, external: true) }
-
- it { is_expected.to contain_exactly(public_group) }
+ with_them do
+ before do
+ # Fixme: Because of an issue: https://github.com/tomykaira/rspec-parameterized/issues/8#issuecomment-381888428
+ # The groups need to be created here, not with let syntax, and also compared by name and not ids
+
+ @groups = {
+ private_group: create(:group, :private, name: 'private_group'),
+ internal_group: create(:group, :internal, name: 'internal_group'),
+ public_group: create(:group, :public, name: 'public_group'),
+
+ user_private_group: create(:group, :private, name: 'user_private_group'),
+ user_internal_group: create(:group, :internal, name: 'user_internal_group'),
+ user_public_group: create(:group, :public, name: 'user_public_group')
+ }
+
+ if user_type
+ user =
+ case user_type
+ when :regular
+ create(:user)
+ when :external
+ create(:user, external: true)
+ when :admin
+ create(:user, :admin)
+ end
+ @groups.values_at(:user_private_group, :user_internal_group, :user_public_group).each do |group|
+ group.add_developer(user)
+ end
+ end
end
- context 'user is member of the private group' do
- before do
- private_group.add_guest(user)
- end
+ subject { described_class.new(User.last, params).execute.to_a }
- it { is_expected.to contain_exactly(public_group, internal_group, private_group) }
- end
+ it { is_expected.to match_array(@groups.values_at(*results)) }
end
end
context 'subgroups', :nested_groups do
+ let(:user) { create(:user) }
let!(:parent_group) { create(:group, :public) }
let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
let!(:internal_subgroup) { create(:group, :internal, parent: parent_group) }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 45439640ea3..74e91b02f0f 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -372,7 +372,7 @@ describe IssuesFinder do
end
context 'personal scope' do
- let(:scope) { 'assigned-to-me' }
+ let(:scope) { 'assigned_to_me' }
it 'returns issue assigned to the user' do
expect(issues).to contain_exactly(issue1, issue2)
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index 5e52898e9c0..00c551a1f65 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -4,14 +4,16 @@ describe PersonalProjectsFinder do
let(:source_user) { create(:user) }
let(:current_user) { create(:user) }
let(:finder) { described_class.new(source_user) }
- let!(:public_project) { create(:project, :public, namespace: source_user.namespace) }
+ let!(:public_project) do
+ create(:project, :public, namespace: source_user.namespace, updated_at: 1.hour.ago)
+ end
let!(:private_project) do
- create(:project, :private, namespace: source_user.namespace, path: 'mepmep')
+ create(:project, :private, namespace: source_user.namespace, updated_at: 3.hours.ago, path: 'mepmep')
end
let!(:internal_project) do
- create(:project, :internal, namespace: source_user.namespace, path: 'C')
+ create(:project, :internal, namespace: source_user.namespace, updated_at: 2.hours.ago, path: 'C')
end
before do
@@ -28,7 +30,7 @@ describe PersonalProjectsFinder do
subject { finder.execute(current_user) }
context 'normal user' do
- it { is_expected.to eq([internal_project, private_project, public_project]) }
+ it { is_expected.to eq([public_project, internal_project, private_project]) }
end
context 'external' do
@@ -36,7 +38,7 @@ describe PersonalProjectsFinder do
current_user.update_attributes(external: true)
end
- it { is_expected.to eq([private_project, public_project]) }
+ it { is_expected.to eq([public_project, private_project]) }
end
end
end
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index 2b19cda35b0..d6253b605b9 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -203,5 +203,25 @@ describe PipelinesFinder do
end
end
end
+
+ context 'when sha is specified' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: '97de212e80737a608d939f648d959671fb0a0142') }
+
+ context 'when sha exists' do
+ let(:params) { { sha: '97de212e80737a608d939f648d959671fb0a0142' } }
+
+ it 'returns matched pipelines' do
+ is_expected.to eq([pipeline])
+ end
+ end
+
+ context 'when sha does not exist' do
+ let(:params) { { sha: 'invalid-sha' } }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
end
diff --git a/spec/fixtures/api/schemas/ci_detailed_status.json b/spec/fixtures/api/schemas/ci_detailed_status.json
new file mode 100644
index 00000000000..01e34249bf1
--- /dev/null
+++ b/spec/fixtures/api/schemas/ci_detailed_status.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required" : [
+ "icon",
+ "text",
+ "label",
+ "group",
+ "tooltip",
+ "has_details",
+ "details_path",
+ "favicon"
+ ],
+ "properties": {
+ "icon": { "type": "string" },
+ "text": { "type": "string" },
+ "label": { "type": "string" },
+ "group": { "type": "string" },
+ "tooltip": { "type": "string" },
+ "has_details": { "type": "boolean" },
+ "details_path": { "type": "string" },
+ "favicon": { "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 a622bf88b13..233102c4314 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -22,6 +22,7 @@
"in_progress_merge_commit_sha": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_commit_sha": { "type": ["string", "null"] },
+ "short_merge_commit_sha": { "type": ["string", "null"] },
"merge_params": { "type": ["object", "null"] },
"merge_status": { "type": "string" },
"merge_user_id": { "type": ["integer", "null"] },
@@ -100,6 +101,7 @@
"merge_commit_message_with_description": { "type": "string" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
+ "merge_commit_path": { "type": ["string", "null"] },
"remove_wip_path": { "type": ["string", "null"] },
"commits_count": { "type": "integer" },
"remove_source_branch": { "type": ["boolean", "null"] },
diff --git a/spec/fixtures/api/schemas/job.json b/spec/fixtures/api/schemas/job.json
new file mode 100644
index 00000000000..7b92ab25bc1
--- /dev/null
+++ b/spec/fixtures/api/schemas/job.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "started",
+ "build_path",
+ "playable",
+ "created_at",
+ "updated_at",
+ "status"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "started": { "type": "boolean" } ,
+ "build_path": { "type": "string" },
+ "playable": { "type": "boolean" },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": "string" },
+ "status": { "$ref": "ci_detailed_status.json" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index 622a1e40d07..05922df6b81 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -17,6 +17,7 @@
"required": [
"id",
"color",
+ "text_color",
"description",
"title",
"priority"
@@ -29,6 +30,7 @@
},
"description": { "type": ["string", "null"] },
"title": { "type": "string" },
+ "title": { "text_color": "string" },
"priority": { "type": ["integer", "null"] }
}
},
diff --git a/spec/fixtures/api/schemas/pipeline_stage.json b/spec/fixtures/api/schemas/pipeline_stage.json
new file mode 100644
index 00000000000..55454200bb3
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_stage.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "title",
+ "status",
+ "path",
+ "dropdown_path"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "title": { "type": "string" },
+ "groups": { "optional": true },
+ "latest_statuses": {
+ "type": "array",
+ "items": { "$ref": "job.json" },
+ "optional": true
+ },
+ "status": { "$ref": "ci_detailed_status.json" },
+ "path": { "type": "string" },
+ "dropdown_path": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json
index 4c4ca3b582f..0f9c221fd6d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/notes.json
+++ b/spec/fixtures/api/schemas/public_api/v4/notes.json
@@ -24,7 +24,10 @@
"system": { "type": "boolean" },
"noteable_id": { "type": "integer" },
"noteable_iid": { "type": "integer" },
- "noteable_type": { "type": "string" }
+ "noteable_type": { "type": "string" },
+ "resolved": { "type": "boolean" },
+ "resolvable": { "type": "boolean" },
+ "resolved_by": { "type": ["string", "null"] }
},
"required": [
"id", "body", "attachment", "author", "created_at", "updated_at",
diff --git a/spec/fixtures/api/schemas/public_api/v4/projects.json b/spec/fixtures/api/schemas/public_api/v4/projects.json
index d89eeea89a5..17ad8d8c48d 100644
--- a/spec/fixtures/api/schemas/public_api/v4/projects.json
+++ b/spec/fixtures/api/schemas/public_api/v4/projects.json
@@ -20,6 +20,7 @@
"ssh_url_to_repo": { "type": "string" },
"http_url_to_repo": { "type": "string" },
"web_url": { "type": "string" },
+ "readme_url": { "type": ["string", "null"] },
"avatar_url": { "type": ["string", "null"] },
"star_count": { "type": "integer" },
"forks_count": { "type": "integer" },
diff --git a/spec/fixtures/emails/valid_new_issue_with_quote.eml b/spec/fixtures/emails/valid_new_issue_with_quote.eml
new file mode 100644
index 00000000000..0caf8ed4e9e
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_issue_with_quote.eml
@@ -0,0 +1,25 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+The reply by email functionality should be extended to allow creating a new issue by email.
+even when the email is forwarded to the project which may include lines that begin with ">"
+
+there should be a quote below this line:
+
+> this is a quote \ No newline at end of file
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
index 352384f16c8..bef7e2ff8ee 100644
--- a/spec/fixtures/exported-project.gz
+++ b/spec/fixtures/exported-project.gz
Binary files differ
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 43cb0dfe163..593b2ca1825 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -2,8 +2,6 @@
require 'spec_helper'
describe ApplicationHelper do
- include UploadHelpers
-
describe 'current_controller?' do
it 'returns true when controller matches argument' do
stub_controller_name('foo')
@@ -54,143 +52,6 @@ describe ApplicationHelper do
end
end
- describe 'project_icon' do
- it 'returns an url for the avatar' do
- project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
-
- expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
- end
- end
-
- describe 'avatar_icon_for' do
- let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
- let(:email) { 'foo@example.com' }
- let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) }
-
- it 'prefers the user to retrieve the avatar_url' do
- expect(helper.avatar_icon_for(user, email).to_s)
- .to eq(user.avatar.url)
- end
-
- it 'falls back to email lookup if no user given' do
- expect(helper.avatar_icon_for(nil, email).to_s)
- .to eq(another_user.avatar.url)
- end
- end
-
- describe 'avatar_icon_for_email' do
- let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
-
- context 'using an email' do
- context 'when there is a matching user' do
- it 'returns a relative URL for the avatar' do
- expect(helper.avatar_icon_for_email(user.email).to_s)
- .to eq(user.avatar.url)
- end
- end
-
- context 'when no user exists for the email' do
- it 'calls gravatar_icon' do
- expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
-
- helper.avatar_icon_for_email('foo@example.com', 20, 2)
- end
- end
-
- context 'without an email passed' do
- it 'calls gravatar_icon' do
- expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
-
- helper.avatar_icon_for_email(nil, 20, 2)
- end
- end
- end
- end
-
- describe 'avatar_icon_for_user' do
- let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
-
- context 'with a user object passed' do
- it 'returns a relative URL for the avatar' do
- expect(helper.avatar_icon_for_user(user).to_s)
- .to eq(user.avatar.url)
- end
- end
-
- context 'without a user object passed' do
- it 'calls gravatar_icon' do
- expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
-
- helper.avatar_icon_for_user(nil, 20, 2)
- end
- end
- end
-
- describe 'gravatar_icon' do
- let(:user_email) { 'user@email.com' }
-
- context 'with Gravatar disabled' do
- before do
- stub_application_setting(gravatar_enabled?: false)
- end
-
- it 'returns a generic avatar' do
- expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
- end
- end
-
- context 'with Gravatar enabled' do
- before do
- stub_application_setting(gravatar_enabled?: true)
- end
-
- it 'returns a generic avatar when email is blank' do
- expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
- end
-
- it 'returns a valid Gravatar URL' do
- stub_config_setting(https: false)
-
- expect(helper.gravatar_icon(user_email))
- .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
- end
-
- it 'uses HTTPs when configured' do
- stub_config_setting(https: true)
-
- expect(helper.gravatar_icon(user_email))
- .to match('https://secure.gravatar.com')
- end
-
- it 'returns custom gravatar path when gravatar_url is set' do
- stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
-
- expect(gravatar_icon(user_email, 20))
- .to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118')
- end
-
- it 'accepts a custom size argument' do
- expect(helper.gravatar_icon(user_email, 64)).to include '?s=128'
- end
-
- it 'defaults size to 40@2x when given an invalid size' do
- expect(helper.gravatar_icon(user_email, nil)).to include '?s=80'
- end
-
- it 'accepts a scaling factor' do
- expect(helper.gravatar_icon(user_email, 40, 3)).to include '?s=120'
- end
-
- it 'ignores case and surrounding whitespace' do
- normal = helper.gravatar_icon('foo@example.com')
- upcase = helper.gravatar_icon(' FOO@EXAMPLE.COM ')
-
- expect(normal).to eq upcase
- end
- end
- end
-
describe 'simple_sanitize' do
let(:a_tag) { '<a href="#">Foo</a>' }
@@ -290,4 +151,16 @@ describe ApplicationHelper do
end
end
end
+
+ describe '#autocomplete_data_sources' do
+ let(:project) { create(:project) }
+ let(:noteable_type) { Issue }
+ it 'returns paths for autocomplete_sources_controller' do
+ sources = helper.autocomplete_data_sources(project, noteable_type)
+ expect(sources.keys).to match_array([:members, :issues, :merge_requests, :labels, :milestones, :commands])
+ sources.keys.each do |key|
+ expect(sources[key]).not_to be_nil
+ end
+ end
+ end
end
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index c94fedd615b..120b23e66ac 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -18,6 +18,30 @@ describe AuthHelper do
end
end
+ describe "providers_for_base_controller" do
+ it 'returns all enabled providers from devise' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :github] }
+ expect(helper.providers_for_base_controller).to include(*[:twitter, :github])
+ end
+
+ it 'excludes ldap providers' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] }
+ expect(helper.providers_for_base_controller).not_to include(:ldapmain)
+ end
+ end
+
+ describe "form_based_providers" do
+ it 'includes LDAP providers' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] }
+ expect(helper.form_based_providers).to eq %i(ldapmain)
+ end
+
+ it 'includes crowd provider' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :crowd] }
+ expect(helper.form_based_providers).to eq %i(crowd)
+ end
+ end
+
describe 'enabled_button_based_providers' do
before do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 04c6d259135..5856bccb5b8 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -1,10 +1,147 @@
require 'rails_helper'
describe AvatarsHelper do
- include ApplicationHelper
+ include UploadHelpers
let(:user) { create(:user) }
+ describe '#project_icon' do
+ it 'returns an url for the avatar' do
+ project = create(:project, :public, avatar: File.open(uploaded_image_temp_path))
+
+ expect(helper.project_icon(project.full_path).to_s)
+ .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
+ end
+ end
+
+ describe '#avatar_icon_for' do
+ let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
+ let(:email) { 'foo@example.com' }
+ let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) }
+
+ it 'prefers the user to retrieve the avatar_url' do
+ expect(helper.avatar_icon_for(user, email).to_s)
+ .to eq(user.avatar.url)
+ end
+
+ it 'falls back to email lookup if no user given' do
+ expect(helper.avatar_icon_for(nil, email).to_s)
+ .to eq(another_user.avatar.url)
+ end
+ end
+
+ describe '#avatar_icon_for_email' do
+ let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
+
+ context 'using an email' do
+ context 'when there is a matching user' do
+ it 'returns a relative URL for the avatar' do
+ expect(helper.avatar_icon_for_email(user.email).to_s)
+ .to eq(user.avatar.url)
+ end
+ end
+
+ context 'when no user exists for the email' do
+ it 'calls gravatar_icon' do
+ expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
+
+ helper.avatar_icon_for_email('foo@example.com', 20, 2)
+ end
+ end
+
+ context 'without an email passed' do
+ it 'calls gravatar_icon' do
+ expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
+
+ helper.avatar_icon_for_email(nil, 20, 2)
+ end
+ end
+ end
+ end
+
+ describe '#avatar_icon_for_user' do
+ let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
+
+ context 'with a user object passed' do
+ it 'returns a relative URL for the avatar' do
+ expect(helper.avatar_icon_for_user(user).to_s)
+ .to eq(user.avatar.url)
+ end
+ end
+
+ context 'without a user object passed' do
+ it 'calls gravatar_icon' do
+ expect(helper).to receive(:gravatar_icon).with(nil, 20, 2)
+
+ helper.avatar_icon_for_user(nil, 20, 2)
+ end
+ end
+ end
+
+ describe '#gravatar_icon' do
+ let(:user_email) { 'user@email.com' }
+
+ context 'with Gravatar disabled' do
+ before do
+ stub_application_setting(gravatar_enabled?: false)
+ end
+
+ it 'returns a generic avatar' do
+ expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
+ end
+ end
+
+ context 'with Gravatar enabled' do
+ before do
+ stub_application_setting(gravatar_enabled?: true)
+ end
+
+ it 'returns a generic avatar when email is blank' do
+ expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
+ end
+
+ it 'returns a valid Gravatar URL' do
+ stub_config_setting(https: false)
+
+ expect(helper.gravatar_icon(user_email))
+ .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
+ end
+
+ it 'uses HTTPs when configured' do
+ stub_config_setting(https: true)
+
+ expect(helper.gravatar_icon(user_email))
+ .to match('https://secure.gravatar.com')
+ end
+
+ it 'returns custom gravatar path when gravatar_url is set' do
+ stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
+
+ expect(gravatar_icon(user_email, 20))
+ .to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118')
+ end
+
+ it 'accepts a custom size argument' do
+ expect(helper.gravatar_icon(user_email, 64)).to include '?s=128'
+ end
+
+ it 'defaults size to 40@2x when given an invalid size' do
+ expect(helper.gravatar_icon(user_email, nil)).to include '?s=80'
+ end
+
+ it 'accepts a scaling factor' do
+ expect(helper.gravatar_icon(user_email, 40, 3)).to include '?s=120'
+ end
+
+ it 'ignores case and surrounding whitespace' do
+ normal = helper.gravatar_icon('foo@example.com')
+ upcase = helper.gravatar_icon(' FOO@EXAMPLE.COM ')
+
+ expect(normal).to eq upcase
+ end
+ end
+ end
+
describe '#user_avatar' do
subject { helper.user_avatar(user: user) }
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 1fa194fe1b8..a3e010c3206 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -242,4 +242,35 @@ describe BlobHelper do
end
end
end
+
+ describe '#ide_edit_path' do
+ let(:project) { create(:project) }
+
+ around do |example|
+ old_script_name = Rails.application.routes.default_url_options[:script_name]
+ begin
+ example.run
+ ensure
+ Rails.application.routes.default_url_options[:script_name] = old_script_name
+ end
+ end
+
+ it 'returns full IDE path' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "master", "")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
+ end
+
+ it 'returns full IDE path with second -' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "testing/slashes", "readme.md")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md")
+ end
+
+ it 'returns IDE path without relative_url_root' do
+ Rails.application.routes.default_url_options[:script_name] = "/gitlab"
+
+ expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
+ end
+ end
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 6c4f7050ee0..143b28728a3 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -89,4 +89,19 @@ describe GitlabRoutingHelper do
expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
end
end
+
+ describe '#edit_milestone_path' do
+ it 'returns group milestone edit path when given entity parent is a Group' do
+ group = create(:group)
+ milestone = create(:milestone, group: group)
+
+ expect(edit_milestone_path(milestone)).to eq("/groups/#{group.path}/-/milestones/#{milestone.iid}/edit")
+ end
+
+ it 'returns project milestone edit path when given entity parent is not a Group' do
+ milestone = create(:milestone, group: nil)
+
+ expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/milestones/#{milestone.iid}/edit")
+ end
+ end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 4224cea4652..cddb49b320f 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -22,11 +22,15 @@ describe IssuablesHelper do
end
describe '#issuable_labels_tooltip' do
- it 'returns label text' do
+ it 'returns label text with no labels' do
+ expect(issuable_labels_tooltip([])).to eq("Labels")
+ end
+
+ it 'returns label text with labels within max limit' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
end
- it 'returns label text' do
+ it 'returns label text with labels exceeding max limit' do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end
end
@@ -41,22 +45,22 @@ describe IssuablesHelper do
it 'returns "Open" when state is :opened' do
expect(helper.issuables_state_counter_text(:issues, :opened, true))
- .to eq('<span>Open</span> <span class="badge">42</span>')
+ .to eq('<span>Open</span> <span class="badge badge-pill">42</span>')
end
it 'returns "Closed" when state is :closed' do
expect(helper.issuables_state_counter_text(:issues, :closed, true))
- .to eq('<span>Closed</span> <span class="badge">42</span>')
+ .to eq('<span>Closed</span> <span class="badge badge-pill">42</span>')
end
it 'returns "Merged" when state is :merged' do
expect(helper.issuables_state_counter_text(:merge_requests, :merged, true))
- .to eq('<span>Merged</span> <span class="badge">42</span>')
+ .to eq('<span>Merged</span> <span class="badge badge-pill">42</span>')
end
it 'returns "All" when state is :all' do
expect(helper.issuables_state_counter_text(:merge_requests, :all, true))
- .to eq('<span>All</span> <span class="badge">42</span>')
+ .to eq('<span>All</span> <span class="badge badge-pill">42</span>')
end
end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 70b4a89cb86..f5185cb2857 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -83,58 +83,4 @@ describe MilestonesHelper do
end
end
end
-
- describe '#milestone_remaining_days' do
- around do |example|
- Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
- end
-
- context 'when less than 31 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
-
- it 'returns days remaining' do
- expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
- end
- end
-
- context 'when less than 1 year and more than 30 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
-
- it 'returns months remaining' do
- expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
- end
- end
-
- context 'when more than 1 year remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
-
- it 'returns years remaining' do
- expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
- end
- end
-
- context 'when milestone is expired' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
-
- it 'returns "Past due"' do
- expect(milestone_remaining).to eq("<strong>Past due</strong>")
- end
- end
-
- context 'when milestone has start_date in the future' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
-
- it 'returns "Upcoming"' do
- expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
- end
- end
-
- context 'when milestone has start_date in the past' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
-
- it 'returns days elapsed' do
- expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
- end
- end
- end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 46c55da24f8..f8877b6d1aa 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -274,16 +274,16 @@ describe ProjectsHelper do
end
end
- describe '#sanitized_import_error' do
+ describe '#sanitizerepo_repo_path' do
let(:project) { create(:project, :repository) }
+ let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
before do
- allow(project).to receive(:repository_storage_path).and_return('/base/repo/path')
allow(Settings.shared).to receive(:[]).with('path').and_return('/base/repo/export/path')
end
it 'removes the repo path' do
- repo = '/base/repo/path/namespace/test.git'
+ repo = File.join(storage_path, 'namespace/test.git')
import_error = "Could not clone #{repo}\n"
expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 6332217b920..b18c045848f 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe UsersHelper do
+ include TermsHelper
+
let(:user) { create(:user) }
describe '#user_link' do
@@ -27,4 +29,39 @@ describe UsersHelper do
expect(tabs).to include(:activity, :groups, :contributed, :projects, :snippets)
end
end
+
+ describe '#current_user_menu_items' do
+ subject(:items) { helper.current_user_menu_items }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(false)
+ end
+
+ it 'includes all default items' do
+ expect(items).to include(:help, :sign_out)
+ end
+
+ it 'includes the profile tab if the user can read themself' do
+ expect(helper).to receive(:can?).with(user, :read_user, user) { true }
+
+ expect(items).to include(:profile)
+ end
+
+ it 'includes the settings tab if the user can update themself' do
+ expect(helper).to receive(:can?).with(user, :read_user, user) { true }
+
+ expect(items).to include(:profile)
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ it 'hides the profile and the settings tab' do
+ expect(items).not_to include(:settings, :profile, :help)
+ end
+ end
+ end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 1dc307ea922..8d9dc092547 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -42,26 +42,6 @@ describe '6_validations' do
expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
end
end
-
- context 'with incomplete settings' do
- before do
- mock_storages('foo' => {})
- end
-
- it 'throws an error suggesting the user to update its settings' do
- expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.')
- end
- end
-
- context 'with deprecated settings structure' do
- before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c')
- end
-
- it 'throws an error suggesting the user to update its settings' do
- expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n")
- end
- end
end
describe 'validate_storages_paths' do
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index 3d922021978..9eb0e732572 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -18,6 +18,7 @@
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
+ "spyOnDependency": false,
"spyOnEvent": false,
"ClassSpecHelper": false
},
diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js
index 909a1bf76bc..5dbdcd24296 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/javascripts/activities_spec.js
@@ -3,24 +3,30 @@
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
import Activities from '~/activities';
+import Pager from '~/pager';
-(() => {
+describe('Activities', () => {
window.gon || (window.gon = {});
const fixtureTemplate = 'static/event_filter.html.raw';
const filters = [
{
id: 'all',
- }, {
+ },
+ {
id: 'push',
name: 'push events',
- }, {
+ },
+ {
id: 'merged',
name: 'merge events',
- }, {
+ },
+ {
id: 'comments',
- }, {
+ },
+ {
id: 'team',
- }];
+ },
+ ];
function getEventName(index) {
const filter = filters[index];
@@ -32,31 +38,34 @@ import Activities from '~/activities';
return `#${filter.id}_event_filter`;
}
- describe('Activities', () => {
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- new Activities();
- });
-
- for (let i = 0; i < filters.length; i += 1) {
- ((i) => {
- describe(`when selecting ${getEventName(i)}`, () => {
- beforeEach(() => {
- $(getSelector(i)).click();
- });
-
- for (let x = 0; x < filters.length; x += 1) {
- ((x) => {
- const shouldHighlight = i === x;
- const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
-
- it(`${testName} ${getEventName(x)}`, () => {
- expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
- });
- })(x);
- }
- });
- })(i);
- }
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ spyOn(Pager, 'init').and.stub();
+ new Activities();
});
-})();
+
+ for (let i = 0; i < filters.length; i += 1) {
+ (i => {
+ describe(`when selecting ${getEventName(i)}`, () => {
+ beforeEach(() => {
+ $(getSelector(i)).click();
+ });
+
+ for (let x = 0; x < filters.length; x += 1) {
+ (x => {
+ const shouldHighlight = i === x;
+ const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+
+ it(`${testName} ${getEventName(x)}`, () => {
+ expect(
+ $(getSelector(x))
+ .parent()
+ .hasClass('active'),
+ ).toEqual(shouldHighlight);
+ });
+ })(x);
+ }
+ });
+ })(i);
+ }
+});
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 3d7ccf432be..e8435116221 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -341,4 +341,25 @@ describe('Api', () => {
.catch(done.fail);
});
});
+
+ describe('commitPipelines', () => {
+ it('fetches pipelines for a given commit', done => {
+ const projectId = 'example/foobar';
+ const commitSha = 'abc123def';
+ const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`;
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
+
+ Api.commitPipelines(projectId, commitSha)
+ .then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].name).toBe('test');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js
index 9439c578973..02e59ae0843 100644
--- a/spec/javascripts/badges/components/badge_list_spec.js
+++ b/spec/javascripts/badges/components/badge_list_spec.js
@@ -33,7 +33,7 @@ describe('BadgeList component', () => {
});
it('renders a header with the badge count', () => {
- const header = vm.$el.querySelector('.panel-heading');
+ const header = vm.$el.querySelector('.card-header');
expect(header).toHaveText(new RegExp(`Your badges\\s+${numberOfDummyBadges}`));
});
diff --git a/spec/javascripts/badges/components/badge_settings_spec.js b/spec/javascripts/badges/components/badge_settings_spec.js
index 3db02982ad4..59367c85125 100644
--- a/spec/javascripts/badges/components/badge_settings_spec.js
+++ b/spec/javascripts/badges/components/badge_settings_spec.js
@@ -60,7 +60,7 @@ describe('BadgeSettings component', () => {
});
it('displays badge list', () => {
- const badgeListElement = vm.$el.querySelector('.panel');
+ const badgeListElement = vm.$el.querySelector('.card');
expect(badgeListElement).not.toBe(null);
expect(badgeListElement).toBeVisible();
expect(badgeListElement).toContainText('Your badges');
@@ -87,7 +87,7 @@ describe('BadgeSettings component', () => {
});
it('displays no badge list', () => {
- const badgeListElement = vm.$el.querySelector('.panel');
+ const badgeListElement = vm.$el.querySelector('.card');
expect(badgeListElement).toBeHidden();
});
});
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index c37c62c63dd..d03836d10f9 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import '~/behaviors/quick_submit';
-describe('Quick Submit behavior', () => {
+describe('Quick Submit behavior', function () {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js
index 38d9bba6868..95122fcf30f 100644
--- a/spec/javascripts/behaviors/secret_values_spec.js
+++ b/spec/javascripts/behaviors/secret_values_spec.js
@@ -9,7 +9,7 @@ function generateValueMarkup(
<div class="${placeholderClass}">
***
</div>
- <div class="hide ${valueClass}">
+ <div class="hidden ${valueClass}">
${secret}
</div>
`;
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
index aa87956109f..cb0f2ba686d 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
@@ -257,9 +257,9 @@ describe('BalsamiqViewer', () => {
name = 'name';
resource = 'resource';
template = `
- <div class="panel panel-default">
- <div class="panel-heading">name</div>
- <div class="panel-body">
+ <div class="card">
+ <div class="card-header">name</div>
+ <div class="card-body">
<img class="img-thumbnail" src="data:image/png;base64,image"/>
</div>
</div>
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
index 0b1de504435..346f795c3f5 100644
--- a/spec/javascripts/blob/blob_file_dropzone_spec.js
+++ b/spec/javascripts/blob/blob_file_dropzone_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import BlobFileDropzone from '~/blob/blob_file_dropzone';
-describe('BlobFileDropzone', () => {
+describe('BlobFileDropzone', function () {
preloadFixtures('blob/show.html.raw');
beforeEach(() => {
diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js
index 79f40559817..e062a068a92 100644
--- a/spec/javascripts/blob/sketch/index_spec.js
+++ b/spec/javascripts/blob/sketch/index_spec.js
@@ -82,7 +82,7 @@ describe('Sketch viewer', () => {
const img = document.querySelector('#js-sketch-viewer img');
expect(img).not.toBeNull();
- expect(img.classList.contains('img-responsive')).toBeTruthy();
+ expect(img.classList.contains('img-fluid')).toBeTruthy();
});
it('renders link to image', () => {
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 03df6c06691..c06b2f60813 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -83,13 +83,13 @@ describe('Board list component', () => {
it('renders issues', () => {
expect(
- component.$el.querySelectorAll('.card').length,
+ component.$el.querySelectorAll('.board-card').length,
).toBe(1);
});
it('sets data attribute with issue id', () => {
expect(
- component.$el.querySelector('.card').getAttribute('data-issue-id'),
+ component.$el.querySelector('.board-card').getAttribute('data-issue-id'),
).toBe('1');
});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index be1ea0b57b4..abeef272c68 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -70,19 +70,19 @@ describe('Issue card component', () => {
it('renders issue title', () => {
expect(
- component.$el.querySelector('.card-title').textContent,
+ component.$el.querySelector('.board-card-title').textContent,
).toContain(issue.title);
});
it('includes issue base in link', () => {
expect(
- component.$el.querySelector('.card-title a').getAttribute('href'),
+ component.$el.querySelector('.board-card-title a').getAttribute('href'),
).toContain('/test');
});
it('includes issue title on link', () => {
expect(
- component.$el.querySelector('.card-title a').getAttribute('title'),
+ component.$el.querySelector('.board-card-title a').getAttribute('title'),
).toBe(issue.title);
});
@@ -105,14 +105,14 @@ describe('Issue card component', () => {
it('renders issue ID with #', () => {
expect(
- component.$el.querySelector('.card-number').textContent,
+ component.$el.querySelector('.board-card-number').textContent,
).toContain(`#${issue.id}`);
});
describe('assignee', () => {
it('does not render assignee', () => {
expect(
- component.$el.querySelector('.card-assignee .avatar'),
+ component.$el.querySelector('.board-card-assignee .avatar'),
).toBeNull();
});
@@ -125,25 +125,25 @@ describe('Issue card component', () => {
it('renders assignee', () => {
expect(
- component.$el.querySelector('.card-assignee .avatar'),
+ component.$el.querySelector('.board-card-assignee .avatar'),
).not.toBeNull();
});
it('sets title', () => {
expect(
- component.$el.querySelector('.card-assignee img').getAttribute('data-original-title'),
+ component.$el.querySelector('.board-card-assignee img').getAttribute('data-original-title'),
).toContain(`Assigned to ${user.name}`);
});
it('sets users path', () => {
expect(
- component.$el.querySelector('.card-assignee a').getAttribute('href'),
+ component.$el.querySelector('.board-card-assignee a').getAttribute('href'),
).toBe('/test');
});
it('renders avatar', () => {
expect(
- component.$el.querySelector('.card-assignee img'),
+ component.$el.querySelector('.board-card-assignee img'),
).not.toBeNull();
});
});
@@ -161,10 +161,10 @@ describe('Issue card component', () => {
it('displays defaults avatar if users avatar is null', () => {
expect(
- component.$el.querySelector('.card-assignee img'),
+ component.$el.querySelector('.board-card-assignee img'),
).not.toBeNull();
expect(
- component.$el.querySelector('.card-assignee img').getAttribute('src'),
+ component.$el.querySelector('.board-card-assignee img').getAttribute('src'),
).toBe('default_avatar');
});
});
@@ -197,7 +197,7 @@ describe('Issue card component', () => {
});
it('renders all four assignees', () => {
- expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(4);
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4);
});
describe('more than four assignees', () => {
@@ -213,11 +213,11 @@ describe('Issue card component', () => {
});
it('renders more avatar counter', () => {
- expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('+2');
+ expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('+2');
});
it('renders three assignees', () => {
- expect(component.$el.querySelectorAll('.card-assignee .avatar').length).toEqual(3);
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
});
it('renders 99+ avatar counter', (done) => {
@@ -232,7 +232,7 @@ describe('Issue card component', () => {
}
Vue.nextTick(() => {
- expect(component.$el.querySelector('.card-assignee .avatar-counter').innerText).toEqual('99+');
+ expect(component.$el.querySelector('.board-card-assignee .avatar-counter').innerText).toEqual('99+');
done();
});
});
@@ -248,13 +248,13 @@ describe('Issue card component', () => {
it('renders list label', () => {
expect(
- component.$el.querySelectorAll('.label').length,
+ component.$el.querySelectorAll('.badge').length,
).toBe(2);
});
it('renders label', () => {
const nodes = [];
- component.$el.querySelectorAll('.label').forEach((label) => {
+ component.$el.querySelectorAll('.badge').forEach((label) => {
nodes.push(label.title);
});
@@ -265,13 +265,13 @@ describe('Issue card component', () => {
it('sets label description as title', () => {
expect(
- component.$el.querySelector('.label').getAttribute('title'),
+ component.$el.querySelector('.badge').getAttribute('title'),
).toContain(label1.description);
});
it('sets background color of button', () => {
const nodes = [];
- component.$el.querySelectorAll('.label').forEach((label) => {
+ component.$el.querySelectorAll('.badge').forEach((label) => {
nodes.push(label.style.backgroundColor);
});
@@ -288,7 +288,7 @@ describe('Issue card component', () => {
Vue.nextTick()
.then(() => {
expect(
- component.$el.querySelectorAll('.label').length,
+ component.$el.querySelectorAll('.badge').length,
).toBe(2);
expect(
component.$el.textContent,
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index 2abf52a1676..8427e8a0ba7 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.catch(done.fail);
});
- it('updates aria-label to mark done', (done) => {
+ it('updates aria-label to mark todo as done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
setTimeout(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
done();
});
@@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.then(() => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ ).toBe('Mark todo as done');
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
})
diff --git a/spec/javascripts/comment_type_toggle_spec.js b/spec/javascripts/comment_type_toggle_spec.js
index dfd0810d52e..0ba709298c5 100644
--- a/spec/javascripts/comment_type_toggle_spec.js
+++ b/spec/javascripts/comment_type_toggle_spec.js
@@ -1,5 +1,4 @@
import CommentTypeToggle from '~/comment_type_toggle';
-import * as dropLabSrc from '~/droplab/drop_lab';
import InputSetter from '~/droplab/plugins/input_setter';
describe('CommentTypeToggle', function () {
@@ -59,14 +58,14 @@ describe('CommentTypeToggle', function () {
this.droplab = jasmine.createSpyObj('droplab', ['init']);
- spyOn(dropLabSrc, 'default').and.returnValue(this.droplab);
+ this.droplabConstructor = spyOnDependency(CommentTypeToggle, 'DropLab').and.returnValue(this.droplab);
spyOn(this.commentTypeToggle, 'setConfig').and.returnValue(this.config);
CommentTypeToggle.prototype.initDroplab.call(this.commentTypeToggle);
});
it('should instantiate a DropLab instance', function () {
- expect(dropLabSrc.default).toHaveBeenCalled();
+ expect(this.droplabConstructor).toHaveBeenCalled();
});
it('should set .droplab', function () {
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 53820770f3f..819ed7896ca 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import pipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-describe('Pipelines table in Commits and Merge requests', () => {
+describe('Pipelines table in Commits and Merge requests', function () {
const jsonFixtureName = 'pipelines/pipelines.json';
let pipeline;
let PipelinesTable;
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index 977298b9221..60d100e8544 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -3,6 +3,7 @@ import 'vendor/jquery.endless-scroll';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import CommitsList from '~/commits';
+import Pager from '~/pager';
describe('Commits List', () => {
let commitsList;
@@ -14,6 +15,7 @@ describe('Commits List', () => {
</form>
<ol id="commits-list"></ol>
`);
+ spyOn(Pager, 'init').and.stub();
commitsList = new CommitsList(25);
});
@@ -68,9 +70,10 @@ describe('Commits List', () => {
mock.restore();
});
- it('should save the last search string', (done) => {
+ it('should save the last search string', done => {
commitsList.searchField.val('GitLab');
- commitsList.filterResults()
+ commitsList
+ .filterResults()
.then(() => {
expect(ajaxSpy).toHaveBeenCalled();
expect(commitsList.lastSearch).toEqual('GitLab');
@@ -80,8 +83,9 @@ describe('Commits List', () => {
.catch(done.fail);
});
- it('should not make ajax call if the input does not change', (done) => {
- commitsList.filterResults()
+ it('should not make ajax call if the input does not change', done => {
+ commitsList
+ .filterResults()
.then(() => {
expect(ajaxSpy).not.toHaveBeenCalled();
expect(commitsList.lastSearch).toEqual('');
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
index 7025c3d836c..5bf72cc0018 100644
--- a/spec/javascripts/deploy_keys/components/action_btn_spec.js
+++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js
@@ -7,62 +7,64 @@ describe('Deploy keys action btn', () => {
const deployKey = data.enabled_keys[0];
let vm;
- beforeEach((done) => {
- const ActionBtnComponent = Vue.extend(actionBtn);
-
- vm = new ActionBtnComponent({
- propsData: {
- deployKey,
- type: 'enable',
+ beforeEach(done => {
+ const ActionBtnComponent = Vue.extend({
+ components: {
+ actionBtn,
+ },
+ data() {
+ return {
+ deployKey,
+ };
},
- }).$mount();
+ template: `
+ <action-btn
+ :deploy-key="deployKey"
+ type="enable">
+ Enable
+ </action-btn>`,
+ });
+
+ vm = new ActionBtnComponent().$mount();
- setTimeout(done);
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
});
- it('renders the type as uppercase', () => {
- expect(
- vm.$el.textContent.trim(),
- ).toBe('Enable');
+ it('renders the default slot', () => {
+ expect(vm.$el.textContent.trim()).toBe('Enable');
});
- it('sends eventHub event with btn type', (done) => {
+ it('sends eventHub event with btn type', done => {
spyOn(eventHub, '$emit');
vm.$el.click();
- setTimeout(() => {
- expect(
- eventHub.$emit,
- ).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
+ Vue.nextTick(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
done();
});
});
- it('shows loading spinner after click', (done) => {
+ it('shows loading spinner after click', done => {
vm.$el.click();
- setTimeout(() => {
- expect(
- vm.$el.querySelector('.fa'),
- ).toBeDefined();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.fa')).toBeDefined();
done();
});
});
- it('disables button after click', (done) => {
+ it('disables button after click', done => {
vm.$el.click();
- setTimeout(() => {
- expect(
- vm.$el.classList.contains('disabled'),
- ).toBeTruthy();
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('disabled')).toBeTruthy();
- expect(
- vm.$el.getAttribute('disabled'),
- ).toBe('disabled');
+ expect(vm.$el.getAttribute('disabled')).toBe('disabled');
done();
});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
index b870f87eab9..183d7cf2d41 100644
--- a/spec/javascripts/deploy_keys/components/app_spec.js
+++ b/spec/javascripts/deploy_keys/components/app_spec.js
@@ -1,26 +1,26 @@
-import _ from 'underscore';
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import eventHub from '~/deploy_keys/eventhub';
import deployKeysApp from '~/deploy_keys/components/app.vue';
+import { TEST_HOST } from 'spec/test_constants';
describe('Deploy keys app component', () => {
const data = getJSONFixture('deploy_keys/keys.json');
let vm;
-
- const deployKeysResponse = (request, next) => {
- next(request.respondWith(JSON.stringify(data), {
- status: 200,
- }));
- };
+ let mock;
beforeEach((done) => {
- const Component = Vue.extend(deployKeysApp);
+ // setup axios mock before component
+ mock = new MockAdapter(axios);
+ mock.onGet(`${TEST_HOST}/dummy/`).replyOnce(200, data);
- Vue.http.interceptors.push(deployKeysResponse);
+ const Component = Vue.extend(deployKeysApp);
vm = new Component({
propsData: {
- endpoint: '/test',
+ endpoint: `${TEST_HOST}/dummy`,
+ projectId: '8',
},
}).$mount();
@@ -28,120 +28,115 @@ describe('Deploy keys app component', () => {
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse);
+ mock.restore();
});
- it('renders loading icon', (done) => {
+ it('renders loading icon', done => {
vm.store.keys = {};
vm.isLoading = false;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(0);
+ expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
- expect(
- vm.$el.querySelector('.fa-spinner'),
- ).toBeDefined();
+ expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
done();
});
});
it('renders keys panels', () => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(3);
+ expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(3);
});
- it('does not render key panels when keys object is empty', (done) => {
- vm.store.keys = {};
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(0);
-
- done();
- });
+ it('renders the titles with keys count', () => {
+ const textContent = selector => {
+ const element = vm.$el.querySelector(`${selector}`);
+
+ expect(element).not.toBeNull();
+ return element.textContent.trim();
+ };
+
+ expect(textContent('.js-deployKeys-tab-enabled_keys')).toContain('Enabled deploy keys');
+ expect(textContent('.js-deployKeys-tab-available_project_keys')).toContain(
+ 'Privately accessible deploy keys',
+ );
+ expect(textContent('.js-deployKeys-tab-public_keys')).toContain(
+ 'Publicly accessible deploy keys',
+ );
+
+ expect(textContent('.js-deployKeys-tab-enabled_keys .badge')).toBe(
+ `${vm.store.keys.enabled_keys.length}`,
+ );
+ expect(textContent('.js-deployKeys-tab-available_project_keys .badge')).toBe(
+ `${vm.store.keys.available_project_keys.length}`,
+ );
+ expect(textContent('.js-deployKeys-tab-public_keys .badge')).toBe(
+ `${vm.store.keys.public_keys.length}`,
+ );
});
- it('does not render public panel when empty', (done) => {
- vm.store.keys.public_keys = [];
+ it('does not render key panels when keys object is empty', done => {
+ vm.store.keys = {};
Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.deploy-keys-panel').length,
- ).toBe(2);
+ expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
done();
});
});
- it('re-fetches deploy keys when enabling a key', (done) => {
+ it('re-fetches deploy keys when enabling a key', done => {
const key = data.public_keys[0];
spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'enableKey').and.callFake(() => new Promise((resolve) => {
- resolve();
-
- setTimeout(() => {
- expect(vm.service.getKeys).toHaveBeenCalled();
-
- done();
- });
- }));
+ spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve());
eventHub.$emit('enable.key', key);
- expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ Vue.nextTick(() => {
+ expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ expect(vm.service.getKeys).toHaveBeenCalled();
+ done();
+ });
});
- it('re-fetches deploy keys when disabling a key', (done) => {
+ it('re-fetches deploy keys when disabling a key', done => {
const key = data.public_keys[0];
spyOn(window, 'confirm').and.returnValue(true);
spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
- resolve();
-
- setTimeout(() => {
- expect(vm.service.getKeys).toHaveBeenCalled();
-
- done();
- });
- }));
+ spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
eventHub.$emit('disable.key', key);
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ Vue.nextTick(() => {
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(vm.service.getKeys).toHaveBeenCalled();
+ done();
+ });
});
- it('calls disableKey when removing a key', (done) => {
+ it('calls disableKey when removing a key', done => {
const key = data.public_keys[0];
spyOn(window, 'confirm').and.returnValue(true);
spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
- resolve();
-
- setTimeout(() => {
- expect(vm.service.getKeys).toHaveBeenCalled();
-
- done();
- });
- }));
+ spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
eventHub.$emit('remove.key', key);
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ Vue.nextTick(() => {
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(vm.service.getKeys).toHaveBeenCalled();
+ done();
+ });
});
it('hasKeys returns true when there are keys', () => {
expect(vm.hasKeys).toEqual(3);
});
- it('resets remove button loading state', (done) => {
+ it('resets disable button loading state', done => {
spyOn(window, 'confirm').and.returnValue(false);
const btn = vm.$el.querySelector('.btn-warning');
@@ -149,7 +144,7 @@ describe('Deploy keys app component', () => {
btn.click();
Vue.nextTick(() => {
- expect(btn.querySelector('.fa')).toBeNull();
+ expect(btn.querySelector('.btn-warning')).not.toExist();
done();
});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
index b7aadf604a4..4279add21d1 100644
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -7,7 +7,7 @@ describe('Deploy keys key', () => {
let vm;
const KeyComponent = Vue.extend(key);
const data = getJSONFixture('deploy_keys/keys.json');
- const createComponent = (deployKey) => {
+ const createComponent = deployKey => {
const store = new DeployKeysStore();
store.keys = data;
@@ -23,37 +23,42 @@ describe('Deploy keys key', () => {
describe('enabled key', () => {
const deployKey = data.enabled_keys[0];
- beforeEach((done) => {
+ beforeEach(done => {
createComponent(deployKey);
setTimeout(done);
});
it('renders the keys title', () => {
- expect(
- vm.$el.querySelector('.title').textContent.trim(),
- ).toContain('My title');
+ expect(vm.$el.querySelector('.title').textContent.trim()).toContain('My title');
});
it('renders human friendly formatted created date', () => {
- expect(
- vm.$el.querySelector('.key-created-at').textContent.trim(),
- ).toBe(`created ${getTimeago().format(deployKey.created_at)}`);
+ expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe(
+ `${getTimeago().format(deployKey.created_at)}`,
+ );
});
- it('shows edit button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
- ).toBe('Edit');
+ it('shows pencil button for editing', () => {
+ expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
});
- it('shows remove button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
- ).toBe('Remove');
+ it('shows disable button when the project is not deletable', () => {
+ expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
});
- it('shows write access title when key has write access', (done) => {
+ it('shows remove button when the project is deletable', done => {
+ vm.deployKey.destroyed_when_orphaned = true;
+ vm.deployKey.almost_orphaned = true;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn .ic-remove')).toExist();
+ done();
+ });
+ });
+ });
+
+ describe('deploy key labels', () => {
+ it('shows write access title when key has write access', done => {
vm.deployKey.deploy_keys_projects[0].can_push = true;
Vue.nextTick(() => {
@@ -64,7 +69,7 @@ describe('Deploy keys key', () => {
});
});
- it('does not show write access title when key has write access', (done) => {
+ it('does not show write access title when key has write access', done => {
vm.deployKey.deploy_keys_projects[0].can_push = false;
Vue.nextTick(() => {
@@ -74,36 +79,73 @@ describe('Deploy keys key', () => {
done();
});
});
+
+ it('shows expandable button if more than two projects', () => {
+ const labels = vm.$el.querySelectorAll('.deploy-project-label');
+ expect(labels.length).toBe(2);
+ expect(labels[1].textContent).toContain('others');
+ expect(labels[1].getAttribute('data-original-title')).toContain('Expand');
+ });
+
+ it('expands all project labels after click', done => {
+ const length = vm.deployKey.deploy_keys_projects.length;
+ vm.$el.querySelectorAll('.deploy-project-label')[1].click();
+
+ Vue.nextTick(() => {
+ const labels = vm.$el.querySelectorAll('.deploy-project-label');
+ expect(labels.length).toBe(length);
+ expect(labels[1].textContent).not.toContain(`+${length} others`);
+ expect(labels[1].getAttribute('data-original-title')).not.toContain('Expand');
+ done();
+ });
+ });
+
+ it('shows two projects', done => {
+ vm.deployKey.deploy_keys_projects = [...vm.deployKey.deploy_keys_projects].slice(0, 2);
+
+ Vue.nextTick(() => {
+ const labels = vm.$el.querySelectorAll('.deploy-project-label');
+ expect(labels.length).toBe(2);
+ expect(labels[1].textContent).toContain(
+ vm.deployKey.deploy_keys_projects[1].project.full_name,
+ );
+ done();
+ });
+ });
});
describe('public keys', () => {
const deployKey = data.public_keys[0];
- beforeEach((done) => {
+ beforeEach(done => {
createComponent(deployKey);
setTimeout(done);
});
- it('shows edit button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
- ).toBe('Edit');
+ it('renders deploy keys without any enabled projects', done => {
+ vm.deployKey.deploy_keys_projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None');
+
+ done();
+ });
});
it('shows enable button', () => {
- expect(
- vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
- ).toBe('Enable');
+ expect(vm.$el.querySelectorAll('.btn')[0].textContent.trim()).toBe('Enable');
});
- it('shows disable button when key is enabled', (done) => {
+ it('shows pencil button for editing', () => {
+ expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
+ });
+
+ it('shows disable button when key is enabled', done => {
vm.store.keys.enabled_keys.push(deployKey);
Vue.nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
- ).toBe('Disable');
+ expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
done();
});
diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
index 08357d2b547..f71f5ccf082 100644
--- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js
+++ b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
@@ -6,7 +6,7 @@ describe('Deploy keys panel', () => {
const data = getJSONFixture('deploy_keys/keys.json');
let vm;
- beforeEach((done) => {
+ beforeEach(done => {
const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
const store = new DeployKeysStore();
store.keys = data;
@@ -24,46 +24,38 @@ describe('Deploy keys panel', () => {
setTimeout(done);
});
- it('renders the title with keys count', () => {
- expect(
- vm.$el.querySelector('h5').textContent.trim(),
- ).toContain('test');
-
- expect(
- vm.$el.querySelector('h5').textContent.trim(),
- ).toContain(`(${vm.keys.length})`);
+ it('renders list of keys', () => {
+ expect(vm.$el.querySelectorAll('.deploy-key').length).toBe(vm.keys.length);
});
- it('renders list of keys', () => {
- expect(
- vm.$el.querySelectorAll('li').length,
- ).toBe(vm.keys.length);
+ it('renders table header', () => {
+ const tableHeader = vm.$el.querySelector('.table-row-header');
+
+ expect(tableHeader).toExist();
+ expect(tableHeader.textContent).toContain('Deploy key');
+ expect(tableHeader.textContent).toContain('Project usage');
+ expect(tableHeader.textContent).toContain('Created');
});
- it('renders help box if keys are empty', (done) => {
+ it('renders help box if keys are empty', done => {
vm.keys = [];
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.settings-message'),
- ).toBeDefined();
+ expect(vm.$el.querySelector('.settings-message')).toBeDefined();
- expect(
- vm.$el.querySelector('.settings-message').textContent.trim(),
- ).toBe('No deploy keys found. Create one with the form above.');
+ expect(vm.$el.querySelector('.settings-message').textContent.trim()).toBe(
+ 'No deploy keys found. Create one with the form above.',
+ );
done();
});
});
- it('does not render help box if keys are empty & showHelpBox is false', (done) => {
+ it('renders no table header if keys are empty', done => {
vm.keys = [];
- vm.showHelpBox = false;
Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.settings-message'),
- ).toBeNull();
+ expect(vm.$el.querySelector('.table-row-header')).not.toExist();
done();
});
diff --git a/spec/javascripts/droplab/hook_spec.js b/spec/javascripts/droplab/hook_spec.js
index 3d39bd0812b..5eed1db2750 100644
--- a/spec/javascripts/droplab/hook_spec.js
+++ b/spec/javascripts/droplab/hook_spec.js
@@ -1,5 +1,4 @@
import Hook from '~/droplab/hook';
-import * as dropdownSrc from '~/droplab/drop_down';
describe('Hook', function () {
describe('class constructor', function () {
@@ -10,7 +9,7 @@ describe('Hook', function () {
this.config = {};
this.dropdown = {};
- spyOn(dropdownSrc, 'default').and.returnValue(this.dropdown);
+ this.dropdownConstructor = spyOnDependency(Hook, 'DropDown').and.returnValue(this.dropdown);
this.hook = new Hook(this.trigger, this.list, this.plugins, this.config);
});
@@ -24,7 +23,7 @@ describe('Hook', function () {
});
it('should call DropDown constructor', function () {
- expect(dropdownSrc.default).toHaveBeenCalledWith(this.list, this.config);
+ expect(this.dropdownConstructor).toHaveBeenCalledWith(this.list, this.config);
});
it('should set .type', function () {
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index e4c3bf2bef1..615168645b4 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -1,8 +1,8 @@
-import _ from 'underscore';
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import environmentsComponent from '~/environments/components/environments_app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import { environment, folder } from './mock_data';
describe('Environment', () => {
@@ -18,103 +18,76 @@ describe('Environment', () => {
let EnvironmentsComponent;
let component;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
EnvironmentsComponent = Vue.extend(environmentsComponent);
});
+ afterEach(() => {
+ component.$destroy();
+ mock.restore();
+ });
+
describe('successfull request', () => {
describe('without environments', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
+ beforeEach((done) => {
+ mock.onGet(mockData.endpoint).reply(200, { environments: [] });
- it('should render the empty state', (done) => {
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
- 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.');
-
done();
}, 0);
});
+
+ it('should render the empty state', () => {
+ 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.');
+ });
});
describe('with paginated environments', () => {
- let backupInterceptors;
- const environmentsResponseInterceptor = (request, next) => {
- next((response) => {
- response.headers.set('X-nExt-pAge', '2');
- });
-
- next(request.respondWith(JSON.stringify({
+ beforeEach((done) => {
+ mock.onGet(mockData.endpoint).reply(200, {
environments: [environment],
stopped_count: 1,
available_count: 0,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '1',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '2',
- },
- }));
- };
-
- beforeEach(() => {
- backupInterceptors = Vue.http.interceptors;
- Vue.http.interceptors = [
- environmentsResponseInterceptor,
- headersInterceptor,
- ];
- component = mountComponent(EnvironmentsComponent, mockData);
- });
+ }, {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ });
- afterEach(() => {
- Vue.http.interceptors = backupInterceptors;
- });
+ component = mountComponent(EnvironmentsComponent, mockData);
- 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(environment.name);
done();
}, 0);
});
+ 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);
+ });
+
describe('pagination', () => {
- it('should render pagination', (done) => {
- setTimeout(() => {
- expect(
- component.$el.querySelectorAll('.gl-pagination li').length,
- ).toEqual(5);
- done();
- }, 0);
+ it('should render pagination', () => {
+ expect(
+ component.$el.querySelectorAll('.gl-pagination li').length,
+ ).toEqual(5);
});
it('should make an API request when page is clicked', (done) => {
@@ -133,50 +106,39 @@ describe('Environment', () => {
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
done();
- });
+ }, 0);
});
});
});
});
describe('unsuccessfull request', () => {
- const environmentsErrorResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
- });
+ beforeEach((done) => {
+ mock.onGet(mockData.endpoint).reply(500, {});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsErrorResponseInterceptor,
- );
- });
-
- it('should render empty state', (done) => {
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
- expect(
- component.$el.querySelector('.js-blank-state-title').textContent,
- ).toContain('You don\'t have any environments right now.');
done();
}, 0);
});
+
+ it('should render empty state', () => {
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+ });
});
describe('expandable folders', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: [folder],
- stopped_count: 0,
- available_count: 1,
- }), {
- status: 200,
- headers: {
+ beforeEach(() => {
+ mock.onGet(mockData.endpoint).reply(200,
+ {
+ environments: [folder],
+ stopped_count: 0,
+ available_count: 1,
+ },
+ {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
@@ -184,18 +146,11 @@ describe('Environment', () => {
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
- }));
- };
+ );
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- component = mountComponent(EnvironmentsComponent, mockData);
- });
+ mock.onGet(environment.folder_path).reply(200, { environments: [environment] });
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
+ component = mountComponent(EnvironmentsComponent, mockData);
});
it('should open a closed folder', (done) => {
@@ -211,7 +166,7 @@ describe('Environment', () => {
).not.toContain('display: none');
done();
});
- });
+ }, 0);
});
it('should close an opened folder', (done) => {
@@ -233,7 +188,7 @@ describe('Environment', () => {
done();
});
});
- });
+ }, 0);
});
it('should show children environments and a button to show all environments', (done) => {
@@ -242,49 +197,32 @@ describe('Environment', () => {
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
- const folderInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
- environments: [environment],
- }), { status: 200 }));
- };
-
- Vue.http.interceptors.push(folderInterceptor);
-
// 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');
-
- Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
done();
});
});
- });
+ }, 0);
});
});
describe('methods', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
+ mock.onGet(mockData.endpoint).reply(200,
+ {
+ environments: [],
+ stopped_count: 0,
+ available_count: 1,
+ },
+ {},
+ );
component = mountComponent(EnvironmentsComponent, mockData);
spyOn(history, 'pushState').and.stub();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
describe('updateContent', () => {
it('should set given parameters', (done) => {
component.updateContent({ scope: 'stopped', page: '3' })
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 906a1116974..f5ce4df0bfe 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,13 +1,15 @@
-import _ from 'underscore';
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
-import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
let Component;
let component;
+ let mock;
+
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
@@ -17,46 +19,35 @@ describe('Environments Folder View', () => {
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
Component = Vue.extend(environmentsFolderViewComponent);
});
afterEach(() => {
+ mock.restore();
+
component.$destroy();
});
describe('successfull request', () => {
- const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify({
+ beforeEach(() => {
+ mock.onGet(mockData.endpoint).reply(200, {
environments: environmentsList,
stopped_count: 1,
available_count: 0,
- }), {
- status: 200,
- headers: {
- 'X-nExt-pAge': '2',
- 'x-page': '1',
- 'X-Per-Page': '2',
- 'X-Prev-Page': '',
- 'X-TOTAL': '20',
- 'X-Total-Pages': '10',
- },
- }));
- };
-
- beforeEach(() => {
- Vue.http.interceptors.push(environmentsResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
+ }, {
+ '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);
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
it('should render a table with environments', (done) => {
setTimeout(() => {
expect(component.$el.querySelectorAll('table')).not.toBeNull();
@@ -135,25 +126,15 @@ describe('Environments Folder View', () => {
});
describe('unsuccessfull request', () => {
- const environmentsErrorResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
- });
+ mock.onGet(mockData.endpoint).reply(500, {
+ environments: [],
+ });
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsErrorResponseInterceptor,
- );
+ component = mountComponent(Component, mockData);
});
it('should not render a table', (done) => {
- component = mountComponent(Component, mockData);
-
setTimeout(() => {
expect(
component.$el.querySelector('table'),
@@ -190,27 +171,15 @@ describe('Environments Folder View', () => {
});
describe('methods', () => {
- const environmentsEmptyResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
- Vue.http.interceptors.push(headersInterceptor);
+ mock.onGet(mockData.endpoint).reply(200, {
+ environments: [],
+ });
component = mountComponent(Component, mockData);
spyOn(history, 'pushState').and.stub();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, environmentsEmptyResponseInterceptor,
- );
- Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
- });
-
describe('updateContent', () => {
it('should set given parameters', (done) => {
component.updateContent({ scope: 'stopped', page: '4' })
diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js
index 15e11aa686b..8a1e26935d9 100644
--- a/spec/javascripts/environments/mock_data.js
+++ b/spec/javascripts/environments/mock_data.js
@@ -82,6 +82,7 @@ export const environment = {
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
+ folder_path: '/root/review-app/environments/7',
},
};
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 95d02974bdc..8fcee36beb8 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,5 +1,3 @@
-import * as urlUtils from '~/lib/utils/url_utility';
-import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
@@ -11,7 +9,7 @@ import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dro
import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
-describe('Filtered Search Manager', () => {
+describe('Filtered Search Manager', function () {
let input;
let manager;
let tokensContainer;
@@ -74,18 +72,19 @@ describe('Filtered Search Manager', () => {
describe('class constructor', () => {
const isLocalStorageAvailable = 'isLocalStorageAvailable';
+ let RecentSearchesStoreSpy;
beforeEach(() => {
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
- spyOn(recentSearchesStoreSrc, 'default');
spyOn(RecentSearchesRoot.prototype, 'render');
+ RecentSearchesStoreSpy = spyOnDependency(FilteredSearchManager, 'RecentSearchesStore');
});
it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
manager = new FilteredSearchManager({ page });
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
- expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({
+ expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({
isLocalStorageAvailable,
allowedKeys: FilteredSearchTokenKeys.getKeys(),
});
@@ -164,7 +163,7 @@ describe('Filtered Search Manager', () => {
it('should search with a single word', (done) => {
input.value = 'searchTerm';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
});
@@ -175,7 +174,7 @@ describe('Filtered Search Manager', () => {
it('should search with multiple words', (done) => {
input.value = 'awesome search terms';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
});
@@ -186,7 +185,7 @@ describe('Filtered Search Manager', () => {
it('should search with special characters', (done) => {
input.value = '~!@#$%^&*()_+{}:<>,.?/';
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
});
@@ -200,7 +199,7 @@ describe('Filtered Search Manager', () => {
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
index d8ba6de5f45..1e6272bad0b 100644
--- a/spec/javascripts/filtered_search/recent_searches_root_spec.js
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -1,11 +1,11 @@
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import * as vueSrc from 'vue';
describe('RecentSearchesRoot', () => {
describe('render', () => {
let recentSearchesRoot;
let data;
let template;
+ let VueSpy;
beforeEach(() => {
recentSearchesRoot = {
@@ -14,7 +14,7 @@ describe('RecentSearchesRoot', () => {
},
};
- spyOn(vueSrc, 'default').and.callFake((options) => {
+ VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake((options) => {
data = options.data;
template = options.template;
});
@@ -23,7 +23,7 @@ describe('RecentSearchesRoot', () => {
});
it('should instantiate Vue', () => {
- expect(vueSrc.default).toHaveBeenCalled();
+ expect(VueSpy).toHaveBeenCalled();
expect(data()).toBe(recentSearchesRoot.store.state);
expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
});
diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb
index 580894ceaf9..24699c3043a 100644
--- a/spec/javascripts/fixtures/deploy_keys.rb
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -7,6 +7,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:project2) { create(:project, :internal)}
+ let(:project3) { create(:project, :internal)}
+ let(:project4) { create(:project, :internal)}
before(:all) do
clean_frontend_fixtures('deploy_keys/')
@@ -28,6 +30,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
create(:deploy_keys_project, project: project, deploy_key: project_key)
create(:deploy_keys_project, project: project2, deploy_key: internal_key)
+ create(:deploy_keys_project, project: project3, deploy_key: project_key)
+ create(:deploy_keys_project, project: project4, deploy_key: project_key)
get :index,
namespace_id: project.namespace.to_param,
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
index 5477c6075f0..aa7af61c7eb 100644
--- a/spec/javascripts/fixtures/event_filter.html.haml
+++ b/spec/javascripts/fixtures/event_filter.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links.event-filter.scrolling-tabs
+%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
%li.active
%a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"}
%span
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
index 397bdc85c67..06ce248dc9c 100644
--- a/spec/javascripts/fixtures/issue_sidebar_label.html.haml
+++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
@@ -1,7 +1,7 @@
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip
.title.hide-collapsed
- %a.edit-link.pull-right{ href: "#" }
+ %a.edit-link.float-right{ href: "#" }
Edit
.selectbox.hide-collapsed{ style: "display: none;" }
.dropdown
diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml
index c38fe8b1f25..632606e0536 100644
--- a/spec/javascripts/fixtures/linked_tabs.html.haml
+++ b/spec/javascripts/fixtures/linked_tabs.html.haml
@@ -1,9 +1,9 @@
-%ul.nav-links.new-session-tabs.linked-tabs
- %li
- %a{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
+%ul.nav.nav-tabs.new-session-tabs.linked-tabs
+ %li.nav-item
+ %a.nav-link{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
Tab 1
- %li
- %a{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } }
+ %li.nav-item
+ %a.nav-link{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } }
Tab 2
.tab-content
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
index b532b48a95b..74584993739 100644
--- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
+++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
@@ -4,6 +4,7 @@
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
%li.js-builds-dropdown-list.scrollable-menu
+ %ul
%li.js-builds-dropdown-loading.hidden
%span.fa.fa-spinner
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 5393502196e..175f386b60e 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -1,9 +1,8 @@
/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
import $ from 'jquery';
-import '~/gl_dropdown';
+import GLDropdown from '~/gl_dropdown';
import '~/lib/utils/common_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
@@ -71,9 +70,9 @@ describe('glDropdown', function describeDropdown() {
it('should open on click', () => {
initDropDown.call(this, false);
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ expect(this.dropdownContainerElement).not.toHaveClass('show');
this.dropdownButtonElement.click();
- expect(this.dropdownContainerElement).toHaveClass('open');
+ expect(this.dropdownContainerElement).toHaveClass('show');
});
it('escapes HTML as text', () => {
@@ -135,28 +134,28 @@ describe('glDropdown', function describeDropdown() {
});
it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
+ expect(this.dropdownContainerElement).toHaveClass('show');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
- spyOn(urlUtils, 'visitUrl').and.stub();
+ const visitUrl = spyOnDependency(GLDropdown, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ expect(this.dropdownContainerElement).not.toHaveClass('show');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation);
+ if (linkedLocation && linkedLocation !== '#') expect(visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
it('should close on ESC keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
+ expect(this.dropdownContainerElement).toHaveClass('show');
this.dropdownContainerElement.trigger({
type: 'keyup',
which: ARROW_KEYS.ESC,
keyCode: ARROW_KEYS.ESC
});
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ expect(this.dropdownContainerElement).not.toHaveClass('show');
});
});
diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js
index 5decb5e6bbd..97c771dcfd3 100644
--- a/spec/javascripts/gpg_badges_spec.js
+++ b/spec/javascripts/gpg_badges_spec.js
@@ -16,8 +16,8 @@ describe('GpgBadges', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
setFixtures(`
- <form
- class="commits-search-form" data-signatures-path="/hello" action="/hello"
+ <form
+ class="commits-search-form js-signature-container" data-signatures-path="/hello" action="/hello"
method="get">
<input name="utf8" type="hidden" value="✓">
<input type="search" name="search" id="commits-search"class="form-control search-text-input input-short">
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index d8428bd0e08..2b92c485f41 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import * as utils from '~/lib/utils/url_utility';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -177,7 +176,7 @@ describe('AppComponent', () => {
it('should fetch groups for provided page details and update window state', (done) => {
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
spyOn(vm, 'updateGroups').and.callThrough();
- spyOn(utils, 'mergeUrlParams').and.callThrough();
+ const mergeUrlParams = spyOnDependency(appComponent, 'mergeUrlParams').and.callThrough();
spyOn(window.history, 'replaceState');
spyOn($, 'scrollTo');
@@ -193,7 +192,7 @@ describe('AppComponent', () => {
setTimeout(() => {
expect(vm.isLoading).toBe(false);
expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
+ expect(mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
expect(window.history.replaceState).toHaveBeenCalledWith({
page: jasmine.any(String),
}, jasmine.any(String), jasmine.any(String));
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index e3c942597a3..49a139855c8 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import eventHub from '~/groups/event_hub';
@@ -135,13 +134,13 @@ describe('GroupItemComponent', () => {
const group = Object.assign({}, mockParentGroupItem);
group.childrenCount = 0;
const newVm = createComponent(group);
- spyOn(urlUtils, 'visitUrl').and.stub();
+ const visitUrl = spyOnDependency(groupItemComponent, 'visitUrl').and.stub();
spyOn(eventHub, '$emit');
newVm.onClickRowGroup(event);
setTimeout(() => {
expect(eventHub.$emit).not.toHaveBeenCalled();
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ expect(visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
done();
}, 0);
});
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js
index 1415ffb7eb3..fa104ae5bcd 100644
--- a/spec/javascripts/helpers/class_spec_helper_spec.js
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js
@@ -2,7 +2,7 @@
import './class_spec_helper';
-describe('ClassSpecHelper', () => {
+describe('ClassSpecHelper', function () {
describe('itShouldBeAStaticMethod', () => {
beforeEach(() => {
class TestClass {
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
index effacbcff4e..a34a1add4e0 100644
--- a/spec/javascripts/helpers/vue_mount_component_helper.js
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -1,14 +1,30 @@
+import Vue from 'vue';
+
+const mountComponent = (Component, props = {}, el = null) => new Component({
+ propsData: props,
+}).$mount(el);
+
export const createComponentWithStore = (Component, store, propsData = {}) => new Component({
store,
propsData,
});
+export const createComponentWithMixin = (mixins = [], state = {}, props = {}, template = '<div></div>') => {
+ const Component = Vue.extend({
+ template,
+ mixins,
+ data() {
+ return props;
+ },
+ });
+
+ return mountComponent(Component, props);
+};
+
export const mountComponentWithStore = (Component, { el, props, store }) =>
new Component({
store,
propsData: props || { },
}).$mount(el);
-export default (Component, props = {}, el = null) => new Component({
- propsData: props,
-}).$mount(el);
+export default mountComponent;
diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js
index 83f29d1b0c2..d6ab0aeeed7 100644
--- a/spec/javascripts/helpers/vuex_action_helper.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
@@ -55,7 +55,7 @@ export default (action, payload, state, expectedMutations, expectedActions, done
};
// call the action with mocked store and arguments
- action({ commit, state, dispatch }, payload);
+ action({ commit, state, dispatch, rootState: state }, payload);
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
diff --git a/spec/javascripts/ide/components/activity_bar_spec.js b/spec/javascripts/ide/components/activity_bar_spec.js
new file mode 100644
index 00000000000..946c7e8e9c8
--- /dev/null
+++ b/spec/javascripts/ide/components/activity_bar_spec.js
@@ -0,0 +1,92 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import { activityBarViews } from '~/ide/constants';
+import ActivityBar from '~/ide/components/activity_bar.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IDE activity bar', () => {
+ const Component = Vue.extend(ActivityBar);
+ let vm;
+
+ beforeEach(() => {
+ Vue.set(store.state.projects, 'abcproject', {
+ web_url: 'testing',
+ });
+ Vue.set(store.state, 'currentProjectId', 'abcproject');
+
+ vm = createComponentWithStore(Component, store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('goBackUrl', () => {
+ it('renders the Go Back link with the referrer when present', () => {
+ const fakeReferrer = '/example/README.md';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm.$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeReferrer);
+ });
+
+ it('renders the Go Back link with the project url when referrer is not present', () => {
+ const fakeReferrer = '';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm.$mount();
+
+ expect(vm.goBackUrl).toEqual('testing');
+ });
+ });
+
+ describe('updateActivityBarView', () => {
+ beforeEach(() => {
+ spyOn(vm, 'updateActivityBarView');
+
+ vm.$mount();
+ });
+
+ it('calls updateActivityBarView with edit value on click', () => {
+ vm.$el.querySelector('.js-ide-edit-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.edit);
+ });
+
+ it('calls updateActivityBarView with commit value on click', () => {
+ vm.$el.querySelector('.js-ide-commit-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.commit);
+ });
+
+ it('calls updateActivityBarView with review value on click', () => {
+ vm.$el.querySelector('.js-ide-review-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(activityBarViews.review);
+ });
+ });
+
+ describe('active item', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ it('sets edit item active', () => {
+ expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
+ });
+
+ it('sets commit item active', done => {
+ vm.$store.state.currentActivityView = activityBarViews.commit;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
index 144e78d14b5..27f10caccb1 100644
--- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -3,6 +3,7 @@ import store from '~/ide/stores';
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from 'spec/ide/helpers';
+import { projectData } from 'spec/ide/mock_data';
describe('IDE commit sidebar actions', () => {
let vm;
@@ -13,6 +14,8 @@ describe('IDE commit sidebar actions', () => {
vm = createComponentWithStore(Component, store);
vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.currentProjectId = 'abcproject';
+ Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData });
vm.$mount();
@@ -32,4 +35,15 @@ describe('IDE commit sidebar actions', () => {
it('renders current branch text', () => {
expect(vm.$el.textContent).toContain('Commit to master branch');
});
+
+ it('hides merge request option when project merge requests are disabled', done => {
+ vm.$store.state.projects.abcproject.merge_requests_enabled = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
+ expect(vm.$el.textContent).not.toContain('Create a new branch and merge request');
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
new file mode 100644
index 00000000000..16d0b354a30
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import emptyState from '~/ide/components/commit_sidebar/empty_state.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('IDE commit panel empty state', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(emptyState);
+
+ Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes');
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders no changes text when last commit message is empty', () => {
+ expect(vm.$el.textContent).toContain('No changes');
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
new file mode 100644
index 00000000000..8b47a365582
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
@@ -0,0 +1,149 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import CommitForm from '~/ide/components/commit_sidebar/form.vue';
+import { activityBarViews } from '~/ide/constants';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
+import { projectData } from 'spec/ide/mock_data';
+import { resetStore } from '../../helpers';
+
+describe('IDE commit form', () => {
+ const Component = Vue.extend(CommitForm);
+ let vm;
+
+ beforeEach(() => {
+ spyOnProperty(window, 'innerHeight').and.returnValue(800);
+
+ store.state.changedFiles.push('test');
+ store.state.currentProjectId = 'abcproject';
+ Vue.set(store.state.projects, 'abcproject', { ...projectData });
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('enables button when has changes', () => {
+ expect(vm.$el.querySelector('[disabled]')).toBe(null);
+ });
+
+ describe('compact', () => {
+ it('renders commit button in compact mode', () => {
+ expect(vm.$el.querySelector('.btn-primary')).not.toBeNull();
+ expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit');
+ });
+
+ it('does not render form', () => {
+ expect(vm.$el.querySelector('form')).toBeNull();
+ });
+
+ it('renders overview text', done => {
+ vm.$store.state.stagedFiles.push('test');
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('p').textContent).toContain('1 unstaged and 1 staged changes');
+ done();
+ });
+ });
+
+ it('shows form when clicking commit button', done => {
+ vm.$el.querySelector('.btn-primary').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('form')).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('toggles activity bar vie when clicking commit button', done => {
+ vm.$el.querySelector('.btn-primary').click();
+
+ vm.$nextTick(() => {
+ expect(store.state.currentActivityView).toBe(activityBarViews.commit);
+
+ done();
+ });
+ });
+ });
+
+ describe('full', () => {
+ beforeEach(done => {
+ vm.isCompact = false;
+
+ vm.$nextTick(done);
+ });
+
+ it('updates commitMessage in store on input', done => {
+ const textarea = vm.$el.querySelector('textarea');
+
+ textarea.value = 'testing commit message';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updating currentActivityView not to commit view sets compact mode', done => {
+ store.state.currentActivityView = 'a';
+
+ vm.$nextTick(() => {
+ expect(vm.isCompact).toBe(true);
+
+ done();
+ });
+ });
+
+ describe('discard draft button', () => {
+ it('hidden when commitMessage is empty', () => {
+ expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
+ });
+
+ it('resets commitMessage when clicking discard button', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.btn-default').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when submitting', () => {
+ beforeEach(() => {
+ spyOn(vm, 'commitChanges');
+ vm.$store.state.stagedFiles.push('test');
+ });
+
+ it('calls commitChanges', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.btn-success').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.commitChanges).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
index 32dbc3bf72e..9af3c15a4e3 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -11,10 +11,17 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
beforeEach(() => {
const Component = Vue.extend(listCollapsed);
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.changedFiles.push(file('file1'), file('file2'));
- vm.$store.state.changedFiles[0].tempFile = true;
+ vm = createComponentWithStore(Component, store, {
+ files: [
+ {
+ ...file('file1'),
+ tempFile: true,
+ },
+ file('file2'),
+ ],
+ iconName: 'staged',
+ title: 'Staged',
+ });
vm.$mount();
});
@@ -26,4 +33,40 @@ describe('Multi-file editor commit sidebar list collapsed', () => {
it('renders added & modified files count', () => {
expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1');
});
+
+ describe('addedFilesLength', () => {
+ it('returns an length of temp files', () => {
+ expect(vm.addedFilesLength).toBe(1);
+ });
+ });
+
+ describe('modifiedFilesLength', () => {
+ it('returns an length of modified files', () => {
+ expect(vm.modifiedFilesLength).toBe(1);
+ });
+ });
+
+ describe('addedFilesIconClass', () => {
+ it('includes multi-file-addition when addedFiles is not empty', () => {
+ expect(vm.addedFilesIconClass).toContain('multi-file-addition');
+ });
+
+ it('excludes multi-file-addition when addedFiles is empty', () => {
+ vm.files = [];
+
+ expect(vm.addedFilesIconClass).not.toContain('multi-file-addition');
+ });
+ });
+
+ describe('modifiedFilesClass', () => {
+ it('includes multi-file-modified when addedFiles is not empty', () => {
+ expect(vm.modifiedFilesClass).toContain('multi-file-modified');
+ });
+
+ it('excludes multi-file-modified when addedFiles is empty', () => {
+ vm.files = [];
+
+ expect(vm.modifiedFilesClass).not.toContain('multi-file-modified');
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index 509434e4300..cc7e0a3f26d 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
+import store from '~/ide/stores';
import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import router from '~/ide/ide_router';
-import store from '~/ide/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
@@ -18,6 +18,7 @@ describe('Multi-file editor commit sidebar list item', () => {
vm = createComponentWithStore(Component, store, {
file: f,
+ actionComponent: 'stage-button',
}).$mount();
});
@@ -31,22 +32,18 @@ describe('Multi-file editor commit sidebar list item', () => {
expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
});
- it('calls discardFileChanges when clicking discard button', () => {
- spyOn(vm, 'discardFileChanges');
-
- vm.$el.querySelector('.multi-file-discard-btn').click();
-
- expect(vm.discardFileChanges).toHaveBeenCalled();
+ it('renders actionn button', () => {
+ expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull();
});
it('opens a closed file in the editor when clicking the file path', done => {
- spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'openPendingTab').and.callThrough();
spyOn(router, 'push');
vm.$el.querySelector('.multi-file-commit-list-path').click();
setTimeout(() => {
- expect(vm.openFileInEditor).toHaveBeenCalled();
+ expect(vm.openPendingTab).toHaveBeenCalled();
expect(router.push).toHaveBeenCalled();
done();
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
index a62c0a28340..54625ef90f8 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import store from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { file } from '../../helpers';
+import { file, resetStore } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
let vm;
@@ -13,6 +13,10 @@ describe('Multi-file editor commit sidebar list', () => {
vm = createComponentWithStore(Component, store, {
title: 'Staged',
fileList: [],
+ iconName: 'staged',
+ action: 'stageAllChanges',
+ actionBtnText: 'stage all',
+ itemActionComponent: 'stage-button',
});
vm.$store.state.rightPanelCollapsed = false;
@@ -22,6 +26,8 @@ describe('Multi-file editor commit sidebar list', () => {
afterEach(() => {
vm.$destroy();
+
+ resetStore(vm.$store);
});
describe('with a list of files', () => {
@@ -38,16 +44,9 @@ describe('Multi-file editor commit sidebar list', () => {
});
});
- describe('collapsed', () => {
- beforeEach(done => {
- vm.$store.state.rightPanelCollapsed = true;
-
- Vue.nextTick(done);
- });
-
- it('hides list', () => {
- expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
- expect(vm.$el.querySelector('.help-block')).toBeNull();
+ describe('empty files array', () => {
+ it('renders no changes text when empty', () => {
+ expect(vm.$el.textContent).toContain('No changes');
});
});
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
new file mode 100644
index 00000000000..6bf8710bda7
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import stageButton from '~/ide/components/commit_sidebar/stage_button.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
+
+describe('IDE stage file button', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(stageButton);
+ f = file();
+
+ vm = createComponentWithStore(Component, store, {
+ path: f.path,
+ });
+
+ spyOn(vm, 'stageChange');
+ spyOn(vm, 'discardFileChanges');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders button to discard & stage', () => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(2);
+ });
+
+ it('calls store with stage button', () => {
+ vm.$el.querySelectorAll('.btn')[0].click();
+
+ expect(vm.stageChange).toHaveBeenCalledWith(f.path);
+ });
+
+ it('calls store with discard button', () => {
+ vm.$el.querySelectorAll('.btn')[1].click();
+
+ expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js b/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js
new file mode 100644
index 00000000000..e1a432b81be
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/success_message_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import successMessage from '~/ide/components/commit_sidebar/success_message.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('IDE commit panel successful commit state', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(successMessage);
+
+ vm = createComponentWithStore(Component, store, {
+ committedStateSvgPath: 'committed-state',
+ });
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders last commit message when it exists', done => {
+ vm.$store.state.lastCommitMsg = 'testing commit message';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('testing commit message');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
new file mode 100644
index 00000000000..917bbb9fb46
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
+
+describe('IDE unstage file button', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(unstageButton);
+ f = file();
+
+ vm = createComponentWithStore(Component, store, {
+ path: f.path,
+ });
+
+ spyOn(vm, 'unstageChange');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders button to unstage', () => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
+ });
+
+ it('calls store with unnstage button', () => {
+ vm.$el.querySelector('.btn').click();
+
+ expect(vm.unstageChange).toHaveBeenCalledWith(f.path);
+ });
+});
diff --git a/spec/javascripts/ide/components/file_finder/index_spec.js b/spec/javascripts/ide/components/file_finder/index_spec.js
new file mode 100644
index 00000000000..4f208e946d2
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/index_spec.js
@@ -0,0 +1,308 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import FindFileComponent from '~/ide/components/file_finder/index.vue';
+import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../../helpers';
+import { mountComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(FindFileComponent);
+ let vm;
+
+ beforeEach(done => {
+ setFixtures('<div id="app"></div>');
+
+ vm = mountComponentWithStore(Component, {
+ store,
+ el: '#app',
+ props: {
+ index: 0,
+ },
+ });
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('with entries', () => {
+ beforeEach(done => {
+ Vue.set(vm.$store.state.entries, 'folder', {
+ ...file('folder'),
+ path: 'folder',
+ type: 'folder',
+ });
+
+ Vue.set(vm.$store.state.entries, 'index.js', {
+ ...file('index.js'),
+ path: 'index.js',
+ type: 'blob',
+ url: '/index.jsurl',
+ });
+
+ Vue.set(vm.$store.state.entries, 'component.js', {
+ ...file('component.js'),
+ path: 'component.js',
+ type: 'blob',
+ });
+
+ setTimeout(done);
+ });
+
+ it('renders list of blobs', () => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).toContain('component.js');
+ expect(vm.$el.textContent).not.toContain('folder');
+ });
+
+ it('filters entries', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('index.js');
+ expect(vm.$el.textContent).not.toContain('component.js');
+
+ done();
+ });
+ });
+
+ it('shows clear button when searchText is not empty', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dropdown-input-clear').classList).toContain('show');
+ expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
+
+ done();
+ });
+ });
+
+ it('clear button resets searchText', done => {
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clear button focues search input', done => {
+ spyOn(vm.$refs.searchInput, 'focus');
+ vm.searchText = 'index';
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.dropdown-input-clear').click();
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('listShowCount', () => {
+ it('returns 1 when no filtered entries exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listShowCount).toBe(1);
+
+ done();
+ });
+ });
+
+ it('returns entries length when not filtered', () => {
+ expect(vm.listShowCount).toBe(2);
+ });
+ });
+
+ describe('listHeight', () => {
+ it('returns 55 when entries exist', () => {
+ expect(vm.listHeight).toBe(55);
+ });
+
+ it('returns 33 when entries dont exist', done => {
+ vm.searchText = 'testing 123';
+
+ vm.$nextTick(() => {
+ expect(vm.listHeight).toBe(33);
+
+ done();
+ });
+ });
+ });
+
+ describe('filteredBlobsLength', () => {
+ it('returns length of filtered blobs', done => {
+ vm.searchText = 'index';
+
+ vm.$nextTick(() => {
+ expect(vm.filteredBlobsLength).toBe(1);
+
+ done();
+ });
+ });
+ });
+
+ describe('watches', () => {
+ describe('searchText', () => {
+ it('resets focusedIndex when updated', done => {
+ vm.focusedIndex = 1;
+ vm.searchText = 'test';
+
+ vm.$nextTick(() => {
+ expect(vm.focusedIndex).toBe(0);
+
+ done();
+ });
+ });
+ });
+
+ describe('fileFindVisible', () => {
+ it('returns searchText when false', done => {
+ vm.searchText = 'test';
+ vm.$store.state.fileFindVisible = true;
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.$store.state.fileFindVisible = false;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.searchText).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('openFile', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ spyOn(vm, 'toggleFileFinder');
+ });
+
+ it('closes file finder', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ });
+
+ it('pushes to router', () => {
+ vm.openFile(vm.$store.state.entries['index.js']);
+
+ expect(router.push).toHaveBeenCalledWith('/project/index.jsurl');
+ });
+ });
+
+ describe('onKeyup', () => {
+ it('opens file on enter key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ENTER_KEY_CODE;
+
+ spyOn(vm, 'openFile');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.openFile).toHaveBeenCalledWith(vm.$store.state.entries['index.js']);
+
+ done();
+ });
+ });
+
+ it('closes file finder on esc key', done => {
+ const event = new CustomEvent('keyup');
+ event.keyCode = ESC_KEY_CODE;
+
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$refs.searchInput.dispatchEvent(event);
+
+ vm.$nextTick(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('onKeyDown', () => {
+ let el;
+
+ beforeEach(() => {
+ el = vm.$refs.searchInput;
+ });
+
+ describe('up key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = UP_KEY_CODE;
+
+ it('resets to last index when at top', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+
+ it('minus 1 from focusedIndex', () => {
+ vm.focusedIndex = 1;
+
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+ });
+
+ describe('down key', () => {
+ const event = new CustomEvent('keydown');
+ event.keyCode = DOWN_KEY_CODE;
+
+ it('resets to first index when at bottom', () => {
+ vm.focusedIndex = 1;
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(0);
+ });
+
+ it('adds 1 to focusedIndex', () => {
+ el.dispatchEvent(event);
+
+ expect(vm.focusedIndex).toBe(1);
+ });
+ });
+ });
+ });
+
+ describe('without entries', () => {
+ it('renders loading text when loading', done => {
+ store.state.loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('Loading...');
+
+ done();
+ });
+ });
+
+ it('renders no files text', () => {
+ expect(vm.$el.textContent).toContain('No files found.');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/file_finder/item_spec.js b/spec/javascripts/ide/components/file_finder/item_spec.js
new file mode 100644
index 00000000000..0f1116c6912
--- /dev/null
+++ b/spec/javascripts/ide/components/file_finder/item_spec.js
@@ -0,0 +1,140 @@
+import Vue from 'vue';
+import ItemComponent from '~/ide/components/file_finder/item.vue';
+import { file } from '../../helpers';
+import createComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE File finder item spec', () => {
+ const Component = Vue.extend(ItemComponent);
+ let vm;
+ let localFile;
+
+ beforeEach(() => {
+ localFile = {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ };
+
+ vm = createComponent(Component, {
+ file: localFile,
+ focused: true,
+ searchText: '',
+ index: 0,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file name & path', () => {
+ expect(vm.$el.textContent).toContain('test file');
+ expect(vm.$el.textContent).toContain('test/file');
+ });
+
+ describe('focused', () => {
+ it('adds is-focused class', () => {
+ expect(vm.$el.classList).toContain('is-focused');
+ });
+
+ it('does not have is-focused class when not focused', done => {
+ vm.focused = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).not.toContain('is-focused');
+
+ done();
+ });
+ });
+ });
+
+ describe('changed file icon', () => {
+ it('does not render when not a changed or temp file', () => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ });
+
+ it('renders when a changed file', done => {
+ vm.file.changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders when a temp file', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ it('emits event when clicked', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ });
+
+ describe('path', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-path');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('adds ellipsis to long text', done => {
+ vm.file.path = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ done();
+ });
+ });
+ });
+
+ describe('name', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-name');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('does not add ellipsis to long text', done => {
+ vm.file.name = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js
deleted file mode 100644
index e17b051f137..00000000000
--- a/spec/javascripts/ide/components/ide_context_bar_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import ideContextBar from '~/ide/components/ide_context_bar.vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-
-describe('Multi-file editor right context bar', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(ideContextBar);
-
- vm = createComponentWithStore(Component, store, {
- noChangesStateSvgPath: 'svg',
- committedStateSvgPath: 'svg',
- });
-
- vm.$store.state.rightPanelCollapsed = false;
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('collapsed', () => {
- beforeEach(done => {
- vm.$store.state.rightPanelCollapsed = true;
-
- Vue.nextTick(done);
- });
-
- it('adds collapsed class', () => {
- expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js
deleted file mode 100644
index 9f6cb459f3b..00000000000
--- a/spec/javascripts/ide/components/ide_external_links_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Vue from 'vue';
-import ideExternalLinks from '~/ide/components/ide_external_links.vue';
-import createComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('ide external links component', () => {
- let vm;
- let fakeReferrer;
- let Component;
-
- const fakeProjectUrl = '/project/';
-
- beforeEach(() => {
- Component = Vue.extend(ideExternalLinks);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('goBackUrl', () => {
- it('renders the Go Back link with the referrer when present', () => {
- fakeReferrer = '/example/README.md';
- spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
-
- vm = createComponent(Component, {
- projectUrl: fakeProjectUrl,
- }).$mount();
-
- expect(vm.goBackUrl).toEqual(fakeReferrer);
- });
-
- it('renders the Go Back link with the project url when referrer is not present', () => {
- fakeReferrer = '';
- spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
-
- vm = createComponent(Component, {
- projectUrl: fakeProjectUrl,
- }).$mount();
-
- expect(vm.goBackUrl).toEqual(fakeProjectUrl);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_project_tree_spec.js b/spec/javascripts/ide/components/ide_project_tree_spec.js
deleted file mode 100644
index 657682cb39c..00000000000
--- a/spec/javascripts/ide/components/ide_project_tree_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Vue from 'vue';
-import ProjectTree from '~/ide/components/ide_project_tree.vue';
-import createComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('IDE project tree', () => {
- const Component = Vue.extend(ProjectTree);
- let vm;
-
- beforeEach(() => {
- vm = createComponent(Component, {
- project: {
- id: 1,
- name: 'test',
- web_url: gl.TEST_HOST,
- avatar_url: '',
- branches: [],
- },
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders identicon when projct has no avatar', () => {
- expect(vm.$el.querySelector('.identicon')).not.toBeNull();
- });
-
- it('renders avatar image if project has avatar', done => {
- vm.project.avatar_url = gl.TEST_HOST;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.identicon')).toBeNull();
- expect(vm.$el.querySelector('img.avatar')).not.toBeNull();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js
deleted file mode 100644
index e0fbc90ca61..00000000000
--- a/spec/javascripts/ide/components/ide_repo_tree_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Vue from 'vue';
-import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
-import createComponent from '../../helpers/vue_mount_component_helper';
-import { file } from '../helpers';
-
-describe('IdeRepoTree', () => {
- let vm;
- let tree;
-
- beforeEach(() => {
- const IdeRepoTree = Vue.extend(ideRepoTree);
-
- tree = {
- tree: [file()],
- loading: false,
- };
-
- vm = createComponent(IdeRepoTree, {
- tree,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders a sidebar', () => {
- expect(vm.$el.querySelector('.loading-file')).toBeNull();
- expect(vm.$el.querySelector('.file')).not.toBeNull();
- });
-
- it('renders 3 loading files if tree is loading', done => {
- tree.loading = true;
-
- vm.$nextTick(() => {
- expect(
- vm.$el.querySelectorAll('.multi-file-loading-container').length,
- ).toEqual(3);
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_review_spec.js b/spec/javascripts/ide/components/ide_review_spec.js
new file mode 100644
index 00000000000..b9ee22b7c1a
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_review_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import IdeReview from '~/ide/components/ide_review.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { trimText } from '../../helpers/vue_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IDE review mode', () => {
+ const Component = Vue.extend(IdeReview);
+ let vm;
+
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+
+ describe('merge request', () => {
+ beforeEach(done => {
+ store.state.currentMergeRequestId = '1';
+ store.state.projects.abcproject.mergeRequests['1'] = {
+ iid: 123,
+ web_url: 'testing123',
+ };
+
+ vm.$nextTick(done);
+ });
+
+ it('renders edit dropdown', () => {
+ expect(vm.$el.querySelector('.btn')).not.toBe(null);
+ });
+
+ it('renders merge request link & IID', () => {
+ const link = vm.$el.querySelector('.ide-review-sub-header');
+
+ expect(link.querySelector('a').getAttribute('href')).toBe('testing123');
+ expect(trimText(link.textContent)).toBe('Merge request (!123)');
+ });
+
+ it('changes text to latest changes when viewer is not mrdiff', done => {
+ store.state.viewer = 'diff';
+
+ vm.$nextTick(() => {
+ expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe(
+ 'Latest changes',
+ );
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js
index 699dae1ce2f..20ee20bc1d7 100644
--- a/spec/javascripts/ide/components/ide_side_bar_spec.js
+++ b/spec/javascripts/ide/components/ide_side_bar_spec.js
@@ -1,8 +1,10 @@
import Vue from 'vue';
import store from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
+import { projectData } from '../mock_data';
describe('IdeSidebar', () => {
let vm;
@@ -10,6 +12,9 @@ describe('IdeSidebar', () => {
beforeEach(() => {
const Component = Vue.extend(ideSidebar);
+ store.state.currentProjectId = 'abcproject';
+ store.state.projects.abcproject = projectData;
+
vm = createComponentWithStore(Component, store).$mount();
});
@@ -20,23 +25,33 @@ describe('IdeSidebar', () => {
});
it('renders a sidebar', () => {
- expect(
- vm.$el.querySelector('.multi-file-commit-panel-inner'),
- ).not.toBeNull();
+ expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
});
it('renders loading icon component', done => {
vm.$store.state.loading = true;
vm.$nextTick(() => {
- expect(
- vm.$el.querySelector('.multi-file-loading-container'),
- ).not.toBeNull();
- expect(
- vm.$el.querySelectorAll('.multi-file-loading-container').length,
- ).toBe(3);
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
done();
});
});
+
+ describe('activityBarComponent', () => {
+ it('renders tree component', () => {
+ expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull();
+ });
+
+ it('renders commit component', done => {
+ vm.$store.state.currentActivityView = activityBarViews.commit;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 5bd890094cc..045a60e56a0 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -1,8 +1,10 @@
import Vue from 'vue';
+import Mousetrap from 'mousetrap';
import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
+import { projectData } from '../mock_data';
describe('ide component', () => {
let vm;
@@ -10,6 +12,10 @@ describe('ide component', () => {
beforeEach(() => {
const Component = Vue.extend(ide);
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+
vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg',
@@ -23,11 +29,11 @@ describe('ide component', () => {
resetStore(vm.$store);
});
- it('does not render panel right when no files open', () => {
+ it('does not render right right when no files open', () => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
});
- it('renders panel right when files are open', done => {
+ it('renders right panel when files are open', done => {
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
};
@@ -38,4 +44,74 @@ describe('ide component', () => {
done();
});
});
+
+ describe('file finder', () => {
+ beforeEach(done => {
+ spyOn(vm, 'toggleFileFinder');
+
+ vm.$store.state.fileFindVisible = true;
+
+ vm.$nextTick(done);
+ });
+
+ it('calls toggleFileFinder on `t` key press', done => {
+ Mousetrap.trigger('t');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `command+p` key press', done => {
+ Mousetrap.trigger('command+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls toggleFileFinder on `ctrl+p` key press', done => {
+ Mousetrap.trigger('ctrl+p');
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.toggleFileFinder).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('always allows `command+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'command+p'),
+ ).toBe(false);
+ });
+
+ it('always allows `ctrl+p` to trigger toggleFileFinder', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 'ctrl+p'),
+ ).toBe(false);
+ });
+
+ it('onlys handles `t` when focused in input-field', () => {
+ expect(
+ vm.mousetrapStopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
+ ).toBe(true);
+ });
+
+ it('stops callback in monaco editor', () => {
+ setFixtures('<div class="inputarea"></div>');
+
+ expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js
new file mode 100644
index 00000000000..770dca9cb0f
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_status_bar_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideStatusBar from '~/ide/components/ide_status_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('ideStatusBar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideStatusBar);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.projects.abcproject = projectData;
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders the statusbar', () => {
+ expect(vm.$el.className).toBe('ide-status-bar');
+ });
+
+ describe('mounted', () => {
+ it('triggers a setInterval', () => {
+ expect(vm.intervalId).not.toBe(null);
+ });
+ });
+
+ describe('commitAgeUpdate', () => {
+ beforeEach(function() {
+ jasmine.clock().install();
+ spyOn(vm, 'commitAgeUpdate').and.callFake(() => {});
+ vm.startTimer();
+ });
+
+ afterEach(function() {
+ jasmine.clock().uninstall();
+ });
+
+ it('gets called every second', () => {
+ expect(vm.commitAgeUpdate).not.toHaveBeenCalled();
+
+ jasmine.clock().tick(1100);
+ expect(vm.commitAgeUpdate.calls.count()).toEqual(1);
+
+ jasmine.clock().tick(1000);
+ expect(vm.commitAgeUpdate.calls.count()).toEqual(2);
+ });
+ });
+
+ describe('getCommitPath', () => {
+ it('returns the path to the commit details', () => {
+ expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
new file mode 100644
index 00000000000..4ecbdb8a55e
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -0,0 +1,54 @@
+import Vue from 'vue';
+import IdeTreeList from '~/ide/components/ide_tree_list.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IDE tree list', () => {
+ const Component = Vue.extend(IdeTreeList);
+ let vm;
+
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(Component, store, {
+ viewerType: 'edit',
+ });
+
+ spyOn(vm, 'updateViewer').and.callThrough();
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('updates viewer on mount', () => {
+ expect(vm.updateViewer).toHaveBeenCalledWith('edit');
+ });
+
+ it('renders loading indicator', done => {
+ store.state.trees['abcproject/master'].loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
+
+ done();
+ });
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_tree_spec.js b/spec/javascripts/ide/components/ide_tree_spec.js
new file mode 100644
index 00000000000..97a0a2432f1
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_tree_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import IdeTree from '~/ide/components/ide_tree.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IdeRepoTree', () => {
+ let vm;
+
+ beforeEach(() => {
+ const IdeRepoTree = Vue.extend(IdeTree);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projectData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(IdeRepoTree, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
index e08abe7d849..7b637f37eba 100644
--- a/spec/javascripts/ide/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -32,12 +32,8 @@ describe('new dropdown component', () => {
it('renders new file, upload and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
- expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe(
- 'Upload file',
- );
- expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
- 'New directory',
- );
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
+ expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
@@ -81,4 +77,18 @@ describe('new dropdown component', () => {
.catch(done.fail);
});
});
+
+ describe('dropdownOpen', () => {
+ it('scrolls dropdown into view', done => {
+ spyOn(vm.$refs.dropdownMenu, 'scrollIntoView');
+
+ vm.dropdownOpen = true;
+
+ setTimeout(() => {
+ expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
index a6e1e5a0d35..f362ed4db65 100644
--- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -25,25 +25,17 @@ describe('new file modal component', () => {
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
- expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(
- `Create new ${title}`,
- );
+ expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
- expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(
- `Create ${title}`,
- );
+ expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
it(`sets form label as ${type}`, () => {
- const title = type === 'tree' ? 'Directory' : 'File';
-
- expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(
- `${title} name`,
- );
+ expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe('Name');
});
describe('createEntryInStore', () => {
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 113ade269e9..5e3e00a180b 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import store from '~/ide/stores';
import service from '~/ide/services';
+import router from '~/ide/ide_router';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
describe('RepoCommitSection', () => {
@@ -12,10 +12,10 @@ describe('RepoCommitSection', () => {
function createComponent() {
const Component = Vue.extend(repoCommitSection);
- vm = createComponentWithStore(Component, store, {
- noChangesStateSvgPath: 'svg',
- committedStateSvgPath: 'commitsvg',
- });
+ store.state.noChangesStateSvgPath = 'svg';
+ store.state.committedStateSvgPath = 'commitsvg';
+
+ vm = createComponentWithStore(Component, store);
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.currentBranchId = 'master';
@@ -28,20 +28,40 @@ describe('RepoCommitSection', () => {
},
};
+ const files = [file('file1'), file('file2')].map(f =>
+ Object.assign(f, {
+ type: 'blob',
+ }),
+ );
+
vm.$store.state.rightPanelCollapsed = false;
vm.$store.state.currentBranch = 'master';
- vm.$store.state.changedFiles = [file('file1'), file('file2')];
+ vm.$store.state.changedFiles = [...files];
vm.$store.state.changedFiles.forEach(f =>
Object.assign(f, {
changed: true,
+ content: 'changedFile testing',
+ }),
+ );
+
+ vm.$store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }];
+ vm.$store.state.stagedFiles.forEach(f =>
+ Object.assign(f, {
+ changed: true,
content: 'testing',
}),
);
+ vm.$store.state.changedFiles.forEach(f => {
+ vm.$store.state.entries[f.path] = f;
+ });
+
return vm.$mount();
}
beforeEach(done => {
+ spyOn(router, 'push');
+
vm = createComponent();
spyOn(service, 'getTreeData').and.returnValue(
@@ -75,99 +95,86 @@ describe('RepoCommitSection', () => {
resetStore(vm.$store);
const Component = Vue.extend(repoCommitSection);
- vm = createComponentWithStore(Component, store, {
- noChangesStateSvgPath: 'nochangessvg',
- committedStateSvgPath: 'svg',
- }).$mount();
+ store.state.noChangesStateSvgPath = 'nochangessvg';
+ store.state.committedStateSvgPath = 'svg';
- expect(
- vm.$el.querySelector('.js-empty-state').textContent.trim(),
- ).toContain('No changes');
- expect(
- vm.$el.querySelector('.js-empty-state img').getAttribute('src'),
- ).toBe('nochangessvg');
+ vm = createComponentWithStore(Component, store).$mount();
+
+ expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes');
+ expect(vm.$el.querySelector('.js-empty-state img').getAttribute('src')).toBe('nochangessvg');
});
});
it('renders a commit section', () => {
- const changedFileElements = [
- ...vm.$el.querySelectorAll('.multi-file-commit-list li'),
- ];
- const submitCommit = vm.$el.querySelector('form .btn');
+ const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
+ const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles);
- expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
- expect(changedFileElements.length).toEqual(2);
+ expect(changedFileElements.length).toEqual(4);
changedFileElements.forEach((changedFile, i) => {
- expect(changedFile.textContent.trim()).toContain(
- vm.$store.state.changedFiles[i].path,
- );
+ expect(changedFile.textContent.trim()).toContain(allFiles[i].path);
});
-
- expect(submitCommit.disabled).toBeTruthy();
- expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
});
- it('updates commitMessage in store on input', done => {
- const textarea = vm.$el.querySelector('textarea');
-
- textarea.value = 'testing commit message';
-
- textarea.dispatchEvent(new Event('input'));
-
- getSetTimeoutPromise()
+ it('adds changed files into staged files', done => {
+ vm.$el.querySelector('.multi-file-discard-btn .btn').click();
+ vm
+ .$nextTick()
+ .then(() => vm.$el.querySelector('.multi-file-discard-btn .btn').click())
+ .then(vm.$nextTick)
.then(() => {
- expect(vm.$store.state.commit.commitMessage).toBe(
- 'testing commit message',
+ expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain(
+ 'No changes',
);
})
.then(done)
.catch(done.fail);
});
- describe('discard draft button', () => {
- it('hidden when commitMessage is empty', () => {
- expect(
- vm.$el.querySelector('.multi-file-commit-form .btn-default'),
- ).toBeNull();
+ it('stages a single file', done => {
+ vm.$el.querySelector('.multi-file-discard-btn .btn').click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
+ 1,
+ );
+
+ done();
});
+ });
+
+ it('discards a single file', done => {
+ vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
+ expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
+ 1,
+ );
- it('resets commitMessage when clicking discard button', done => {
- vm.$store.state.commit.commitMessage = 'testing commit message';
-
- getSetTimeoutPromise()
- .then(() => {
- vm.$el.querySelector('.multi-file-commit-form .btn-default').click();
- })
- .then(Vue.nextTick)
- .then(() => {
- expect(vm.$store.state.commit.commitMessage).not.toBe(
- 'testing commit message',
- );
- })
- .then(done)
- .catch(done.fail);
+ done();
});
});
- describe('when submitting', () => {
- beforeEach(() => {
- spyOn(vm, 'commitChanges');
+ it('unstages a single file', done => {
+ vm.$el
+ .querySelectorAll('.multi-file-discard-btn')[2]
+ .querySelector('.btn')
+ .click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.ide-commit-list-container')[1].querySelectorAll('li').length,
+ ).toBe(1);
+
+ done();
});
+ });
- it('calls commitChanges', done => {
- vm.$store.state.commit.commitMessage = 'testing commit message';
-
- getSetTimeoutPromise()
- .then(() => {
- vm.$el.querySelector('.multi-file-commit-form .btn-success').click();
- })
- .then(Vue.nextTick)
- .then(() => {
- expect(vm.commitChanges).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ describe('mounted', () => {
+ it('opens last opened file', () => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].pending).toBe(true);
});
});
});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index 310d222377f..d3f80e6f9c0 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -5,6 +5,7 @@ import store from '~/ide/stores';
import repoEditor from '~/ide/components/repo_editor.vue';
import monacoLoader from '~/ide/monaco_loader';
import Editor from '~/ide/lib/editor';
+import { activityBarViews } from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
@@ -23,7 +24,7 @@ describe('RepoEditor', () => {
f.active = true;
f.tempFile = true;
vm.$store.state.openFiles.push(f);
- vm.$store.state.entries[f.path] = f;
+ Vue.set(vm.$store.state.entries, f.path, f);
vm.monaco = true;
vm.$mount();
@@ -200,7 +201,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
- expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file);
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null);
expect(vm.model).not.toBeNull();
});
@@ -214,6 +215,30 @@ describe('RepoEditor', () => {
expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model);
});
+ it('attaches model to merge request editor', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = true;
+ spyOn(vm.editor, 'attachMergeRequestModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('does not attach model to merge request editor when not a MR change', () => {
+ vm.$store.state.viewer = 'mrdiff';
+ vm.file.mrChange = false;
+ spyOn(vm.editor, 'attachMergeRequestModel');
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachMergeRequestModel).not.toHaveBeenCalledWith(vm.model);
+ });
+
it('adds callback methods', () => {
spyOn(vm.editor, 'onPositionChange').and.callThrough();
@@ -222,7 +247,7 @@ describe('RepoEditor', () => {
vm.setupEditor();
expect(vm.editor.onPositionChange).toHaveBeenCalled();
- expect(vm.model.events.size).toBe(1);
+ expect(vm.model.events.size).toBe(2);
});
it('updates state when model content changed', done => {
@@ -234,6 +259,20 @@ describe('RepoEditor', () => {
done();
});
});
+
+ it('sets head model as staged file', () => {
+ spyOn(vm.editor, 'createModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' });
+ vm.file.staged = true;
+ vm.file.key = `unstaged-${vm.file.key}`;
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]);
+ });
});
describe('editor updateDimensions', () => {
@@ -281,4 +320,50 @@ describe('RepoEditor', () => {
});
});
});
+
+ describe('show tabs', () => {
+ it('shows tabs in edit mode', () => {
+ expect(vm.$el.querySelector('.nav-links')).not.toBe(null);
+ });
+
+ it('hides tabs in review mode', done => {
+ vm.$store.state.currentActivityView = activityBarViews.review;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.nav-links')).toBe(null);
+
+ done();
+ });
+ });
+
+ it('hides tabs in commit mode', done => {
+ vm.$store.state.currentActivityView = activityBarViews.commit;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.nav-links')).toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ it('calls removePendingTab when old file is pending', done => {
+ spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true);
+ spyOn(vm, 'removePendingTab');
+
+ vm.file.pending = true;
+
+ vm
+ .$nextTick()
+ .then(() => {
+ vm.file = file('testing');
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.removePendingTab).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js
index ff391cb4351..156233653ab 100644
--- a/spec/javascripts/ide/components/repo_file_spec.js
+++ b/spec/javascripts/ide/components/repo_file_spec.js
@@ -48,6 +48,70 @@ describe('RepoFile', () => {
});
});
+ describe('folder', () => {
+ it('renders changes count inside folder', () => {
+ const f = {
+ ...file('folder'),
+ path: 'testing',
+ type: 'tree',
+ branchId: 'master',
+ projectId: 'project',
+ };
+
+ store.state.changedFiles.push({
+ ...file('fileName'),
+ path: 'testing/fileName',
+ });
+
+ createComponent({
+ file: f,
+ level: 0,
+ });
+
+ const treeChangesEl = vm.$el.querySelector('.ide-tree-changes');
+
+ expect(treeChangesEl).not.toBeNull();
+ expect(treeChangesEl.textContent).toContain('1');
+ });
+
+ it('renders action dropdown', done => {
+ createComponent({
+ file: {
+ ...file('t4'),
+ type: 'tree',
+ branchId: 'master',
+ projectId: 'project',
+ },
+ level: 0,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('disables action dropdown', done => {
+ createComponent({
+ file: {
+ ...file('t4'),
+ type: 'tree',
+ branchId: 'master',
+ projectId: 'project',
+ },
+ level: 0,
+ disableActionDropdown: true,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.ide-new-btn')).toBeNull();
+
+ done();
+ });
+ });
+ });
+
describe('locked file', () => {
let f;
@@ -72,8 +136,7 @@ describe('RepoFile', () => {
it('renders a tooltip', () => {
expect(
- vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset
- .originalTitle,
+ vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset.originalTitle,
).toContain('Locked by testuser');
});
});
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index cb785ba2cd3..583f71e6121 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -26,60 +26,10 @@ describe('RepoTabs', () => {
const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
expect(tabs.length).toEqual(2);
- expect(tabs[0].classList.contains('active')).toEqual(true);
- expect(tabs[1].classList.contains('active')).toEqual(false);
+ expect(tabs[0].parentNode.classList.contains('active')).toEqual(true);
+ expect(tabs[1].parentNode.classList.contains('active')).toEqual(false);
done();
});
});
-
- describe('updated', () => {
- it('sets showShadow as true when scroll width is larger than width', done => {
- const el = document.createElement('div');
- el.innerHTML = '<div id="test-app"></div>';
- document.body.appendChild(el);
-
- const style = document.createElement('style');
- style.innerText = `
- .multi-file-tabs {
- width: 100px;
- }
-
- .multi-file-tabs .list-unstyled {
- display: flex;
- overflow-x: auto;
- }
- `;
- document.head.appendChild(style);
-
- vm = createComponent(
- RepoTabs,
- {
- files: [],
- viewer: 'editor',
- hasChanges: false,
- activeFile: file('activeFile'),
- hasMergeRequest: false,
- },
- '#test-app',
- );
-
- vm
- .$nextTick()
- .then(() => {
- expect(vm.showShadow).toEqual(false);
-
- vm.files = openedFiles;
- })
- .then(vm.$nextTick)
- .then(() => {
- expect(vm.showShadow).toEqual(true);
-
- style.remove();
- el.remove();
- })
- .then(done)
- .catch(done.fail);
- });
- });
});
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index 8fc2fccb64c..c278bf92b08 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -28,6 +28,23 @@ describe('Multi-file editor library model', () => {
expect(model.originalModel).not.toBeNull();
expect(model.model).not.toBeNull();
expect(model.baseModel).not.toBeNull();
+
+ expect(model.originalModel.uri.path).toBe('original/path--path');
+ expect(model.model.uri.path).toBe('path--path');
+ expect(model.baseModel.uri.path).toBe('target/path--path');
+ });
+
+ it('creates model with head file to compare against', () => {
+ const f = file('path');
+ model.dispose();
+
+ model = new Model(monaco, f, {
+ ...f,
+ content: '123 testing',
+ });
+
+ expect(model.head).not.toBeNull();
+ expect(model.getOriginalModel().getValue()).toBe('123 testing');
});
it('adds eventHub listener', () => {
@@ -70,13 +87,6 @@ describe('Multi-file editor library model', () => {
});
describe('onChange', () => {
- it('caches event by path', () => {
- model.onChange(() => {});
-
- expect(model.events.size).toBe(1);
- expect(model.events.keys().next().value).toBe(model.file.key);
- });
-
it('calls callback on change', done => {
const spy = jasmine.createSpy();
model.onChange(spy);
@@ -119,5 +129,15 @@ describe('Multi-file editor library model', () => {
jasmine.anything(),
);
});
+
+ it('calls onDispose callback', () => {
+ const disposeSpy = jasmine.createSpy();
+
+ model.onDispose(disposeSpy);
+
+ model.dispose();
+
+ expect(disposeSpy).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index aec325e26a9..e1c4ca570b6 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -117,4 +117,33 @@ describe('Multi-file editor library decorations controller', () => {
expect(controller.editorDecorations.size).toBe(0);
});
});
+
+ describe('hasDecorations', () => {
+ it('returns true when decorations are cached', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.hasDecorations(model)).toBe(true);
+ });
+
+ it('returns false when no model decorations exist', () => {
+ expect(controller.hasDecorations(model)).toBe(false);
+ });
+ });
+
+ describe('removeDecorations', () => {
+ beforeEach(() => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.decorate(model);
+ });
+
+ it('removes cached decorations', () => {
+ expect(controller.decorations.size).not.toBe(0);
+ expect(controller.editorDecorations.size).not.toBe(0);
+
+ controller.removeDecorations(model);
+
+ expect(controller.decorations.size).toBe(0);
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index ff73240734e..fd8ab3b4f1d 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -3,10 +3,7 @@ import monacoLoader from '~/ide/monaco_loader';
import editor from '~/ide/lib/editor';
import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
-import DirtyDiffController, {
- getDiffChangeType,
- getDecorator,
-} from '~/ide/lib/diff/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
import { computeDiff } from '~/ide/lib/diff/diff';
import { file } from '../../helpers';
@@ -90,6 +87,14 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(model.onChange).toHaveBeenCalled();
});
+ it('adds dispose event callback', () => {
+ spyOn(model, 'onDispose');
+
+ controller.attachModel(model);
+
+ expect(model.onDispose).toHaveBeenCalled();
+ });
+
it('calls throttledComputeDiff on change', () => {
spyOn(controller, 'throttledComputeDiff');
@@ -99,6 +104,12 @@ describe('Multi-file editor library dirty diff controller', () => {
expect(controller.throttledComputeDiff).toHaveBeenCalled();
});
+
+ it('caches model', () => {
+ controller.attachModel(model);
+
+ expect(controller.models.has(model.url)).toBe(true);
+ });
});
describe('computeDiff', () => {
@@ -116,14 +127,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
describe('reDecorate', () => {
- it('calls decorations controller decorate', () => {
+ it('calls computeDiff when no decorations are cached', () => {
+ spyOn(controller, 'computeDiff');
+
+ controller.reDecorate(model);
+
+ expect(controller.computeDiff).toHaveBeenCalledWith(model);
+ });
+
+ it('calls decorate when decorations are cached', () => {
spyOn(controller.decorationsController, 'decorate');
+ controller.decorationsController.decorations.set(model.url, 'test');
+
controller.reDecorate(model);
- expect(controller.decorationsController.decorate).toHaveBeenCalledWith(
- model,
- );
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
});
});
@@ -133,16 +152,15 @@ describe('Multi-file editor library dirty diff controller', () => {
controller.decorate({ data: { changes: [], path: model.path } });
- expect(
- controller.decorationsController.addDecorations,
- ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything());
+ expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(
+ model,
+ 'dirtyDiff',
+ jasmine.anything(),
+ );
});
it('adds decorations into editor', () => {
- const spy = spyOn(
- controller.decorationsController.editor.instance,
- 'deltaDecorations',
- );
+ const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
controller.decorate({
data: { changes: computeDiff('123', '1234'), path: model.path },
@@ -181,16 +199,22 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('removes worker event listener', () => {
- spyOn(
- controller.dirtyDiffWorker,
- 'removeEventListener',
- ).and.callThrough();
+ spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
controller.dispose();
- expect(
- controller.dirtyDiffWorker.removeEventListener,
- ).toHaveBeenCalledWith('message', jasmine.anything());
+ expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith(
+ 'message',
+ jasmine.anything(),
+ );
+ });
+
+ it('clears cached models', () => {
+ controller.attachModel(model);
+
+ model.dispose();
+
+ expect(controller.models.size).toBe(0);
});
});
});
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 75e6f0f54ec..b88a12264ca 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -74,10 +74,10 @@ describe('Multi-file editor library', () => {
scrollBeyondLastLine: false,
quickSuggestions: false,
occurrencesHighlight: false,
- renderLineHighlight: 'none',
- hideCursorInOverviewRuler: true,
wordWrap: 'on',
renderSideBySide: true,
+ renderLineHighlight: 'all',
+ hideCursorInOverviewRuler: false,
});
});
});
@@ -88,7 +88,7 @@ describe('Multi-file editor library', () => {
instance.createModel('FILE');
- expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null);
});
});
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
new file mode 100644
index 00000000000..c68ae050641
--- /dev/null
+++ b/spec/javascripts/ide/mock_data.js
@@ -0,0 +1,95 @@
+export const projectData = {
+ id: 1,
+ name: 'abcproject',
+ web_url: '',
+ avatar_url: '',
+ path: '',
+ name_with_namespace: 'namespace/abcproject',
+ branches: {
+ master: {
+ treeId: 'abcproject/master',
+ },
+ },
+ mergeRequests: {},
+ merge_requests_enabled: true,
+};
+
+export const pipelines = [
+ {
+ id: 1,
+ ref: 'master',
+ sha: '123',
+ status: 'failed',
+ },
+ {
+ id: 2,
+ ref: 'master',
+ sha: '213',
+ status: 'success',
+ },
+];
+
+export const jobs = [
+ {
+ id: 1,
+ name: 'test',
+ status: 'failed',
+ stage: 'test',
+ duration: 1,
+ },
+ {
+ id: 2,
+ name: 'test 2',
+ status: 'failed',
+ stage: 'test',
+ duration: 1,
+ },
+ {
+ id: 3,
+ name: 'test 3',
+ status: 'failed',
+ stage: 'test',
+ duration: 1,
+ },
+ {
+ id: 4,
+ name: 'test 3',
+ status: 'failed',
+ stage: 'build',
+ duration: 1,
+ },
+];
+
+export const fullPipelinesResponse = {
+ data: {
+ count: {
+ all: 2,
+ },
+ pipelines: [
+ {
+ id: '51',
+ commit: {
+ id: 'xxxxxxxxxxxxxxxxxxxx',
+ },
+ details: {
+ status: {
+ icon: 'status_failed',
+ text: 'failed',
+ },
+ },
+ },
+ {
+ id: '50',
+ commit: {
+ id: 'abc123def456ghi789jkl',
+ },
+ details: {
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
+ },
+ },
+ ],
+ },
+};
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 479ed7ce49e..7bebc2288e3 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
import store from '~/ide/stores';
+import * as actions from '~/ide/stores/actions/file';
+import * as types from '~/ide/stores/mutation_types';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
+import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store file actions', () => {
beforeEach(() => {
@@ -395,6 +398,20 @@ describe('IDE store file actions', () => {
})
.catch(done.fail);
});
+
+ it('bursts unused seal', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(store.state.unusedSeal).toBe(false);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
describe('discardFileChanges', () => {
@@ -402,6 +419,7 @@ describe('IDE store file actions', () => {
beforeEach(() => {
spyOn(eventHub, '$on');
+ spyOn(eventHub, '$emit');
tmpFile = file();
tmpFile.content = 'testing';
@@ -460,6 +478,63 @@ describe('IDE store file actions', () => {
})
.catch(done.fail);
});
+
+ it('pushes route for active file', done => {
+ tmpFile.active = true;
+ store.state.openFiles.push(tmpFile);
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('emits eventHub event to dispose cached model', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('stageChange', () => {
+ it('calls STAGE_CHANGE with file path', done => {
+ testAction(
+ actions.stageChange,
+ 'path',
+ store.state,
+ [
+ { type: types.STAGE_CHANGE, payload: 'path' },
+ { type: types.SET_LAST_COMMIT_MSG, payload: '' },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('unstageChange', () => {
+ it('calls UNSTAGE_CHANGE with file path', done => {
+ testAction(
+ actions.unstageChange,
+ 'path',
+ store.state,
+ [
+ { type: types.UNSTAGE_CHANGE, payload: 'path' },
+ { type: types.SET_LAST_COMMIT_MSG, payload: '' },
+ ],
+ [],
+ done,
+ );
+ });
});
describe('openPendingTab', () => {
@@ -476,7 +551,7 @@ describe('IDE store file actions', () => {
it('makes file pending in openFiles', done => {
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(store.state.openFiles[0].pending).toBe(true);
})
@@ -486,7 +561,7 @@ describe('IDE store file actions', () => {
it('returns true when opened', done => {
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(added => {
expect(added).toBe(true);
})
@@ -494,11 +569,27 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
+ it('returns false when already opened', done => {
+ store.state.openFiles.push({
+ ...f,
+ active: true,
+ key: `pending-${f.key}`,
+ });
+
+ store
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
+ .then(added => {
+ expect(added).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
it('pushes router URL when added', done => {
store.state.currentBranchId = 'master';
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/');
})
@@ -512,7 +603,7 @@ describe('IDE store file actions', () => {
store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
store
- .dispatch('openPendingTab', f)
+ .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
.then(() => {
expect(scrollToTabSpy).toHaveBeenCalled();
store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
@@ -520,20 +611,6 @@ describe('IDE store file actions', () => {
.then(done)
.catch(done.fail);
});
-
- it('returns false when passed in file is active & viewer is diff', done => {
- f.active = true;
- store.state.openFiles.push(f);
- store.state.viewer = 'diff';
-
- store
- .dispatch('openPendingTab', f)
- .then(added => {
- expect(added).toBe(false);
- })
- .then(done)
- .catch(done.fail);
- });
});
describe('removePendingTab', () => {
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
new file mode 100644
index 00000000000..8e078ae7138
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -0,0 +1,192 @@
+import Visibility from 'visibilityjs';
+import MockAdapter from 'axios-mock-adapter';
+import { refreshLastCommitData, pollSuccessCallBack } from '~/ide/stores/actions';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import axios from '~/lib/utils/axios_utils';
+import { fullPipelinesResponse } from '../../mock_data';
+import { resetStore } from '../../helpers';
+import testAction from '../../../helpers/vuex_action_helper';
+
+describe('IDE store project actions', () => {
+ const setProjectState = () => {
+ store.state.currentProjectId = 'abc/def';
+ store.state.currentBranchId = 'master';
+ store.state.projects['abc/def'] = {
+ id: 4,
+ path_with_namespace: 'abc/def',
+ branches: {
+ master: {
+ commit: {
+ id: 'abc123def456ghi789jkl',
+ title: 'example',
+ },
+ },
+ },
+ };
+ };
+
+ beforeEach(() => {
+ store.state.projects['abc/def'] = {};
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('refreshLastCommitData', () => {
+ beforeEach(() => {
+ store.state.currentProjectId = 'abc/def';
+ store.state.currentBranchId = 'master';
+ store.state.projects['abc/def'] = {
+ id: 4,
+ branches: {
+ master: {
+ commit: null,
+ },
+ },
+ };
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+ });
+
+ it('calls the service', done => {
+ store
+ .dispatch('refreshLastCommitData', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ })
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('commits getBranchData', done => {
+ testAction(
+ refreshLastCommitData,
+ {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ },
+ store.state,
+ [
+ {
+ type: 'SET_BRANCH_COMMIT',
+ payload: {
+ projectId: 'abc/def',
+ branchId: 'master',
+ commit: { id: '123' },
+ },
+ },
+ ], // mutations
+ [
+ {
+ type: 'getLastCommitPipeline',
+ payload: {
+ projectId: 'abc/def',
+ projectIdNumber: store.state.projects['abc/def'].id,
+ branchId: 'master',
+ },
+ },
+ ], // action
+ done,
+ );
+ });
+ });
+
+ describe('pipelinePoll', () => {
+ let mock;
+
+ beforeEach(() => {
+ setProjectState();
+ jasmine.clock().install();
+ mock = new MockAdapter(axios);
+ mock
+ .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
+ .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ mock.restore();
+ store.dispatch('stopPipelinePolling');
+ });
+
+ it('calls service periodically', done => {
+ spyOn(axios, 'get').and.callThrough();
+ spyOn(Visibility, 'hidden').and.returnValue(false);
+
+ store
+ .dispatch('pipelinePoll')
+ .then(() => {
+ jasmine.clock().tick(1000);
+
+ expect(axios.get).toHaveBeenCalled();
+ expect(axios.get.calls.count()).toBe(1);
+ })
+ .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
+ .then(() => {
+ jasmine.clock().tick(10000);
+ expect(axios.get.calls.count()).toBe(2);
+ })
+ .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
+ .then(() => {
+ jasmine.clock().tick(10000);
+ expect(axios.get.calls.count()).toBe(3);
+ })
+ .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
+ .then(() => {
+ jasmine.clock().tick(10000);
+ expect(axios.get.calls.count()).toBe(4);
+ })
+
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('pollSuccessCallBack', () => {
+ beforeEach(() => {
+ setProjectState();
+ });
+
+ it('commits correct pipeline', done => {
+ testAction(
+ pollSuccessCallBack,
+ fullPipelinesResponse,
+ store.state,
+ [
+ {
+ type: 'SET_LAST_COMMIT_PIPELINE',
+ payload: {
+ projectId: 'abc/def',
+ branchId: 'master',
+ pipeline: {
+ id: '50',
+ commit: {
+ id: 'abc123def456ghi789jkl',
+ },
+ details: {
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
+ },
+ },
+ },
+ },
+ ], // mutations
+ [], // action
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index cec572f4507..062c3497623 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -1,7 +1,17 @@
-import * as urlUtils from '~/lib/utils/url_utility';
+import actions, {
+ stageAllChanges,
+ unstageAllChanges,
+ toggleFileFinder,
+ setCurrentBranchId,
+ setEmptyStateSvgs,
+ updateActivityBarView,
+ updateTempFlagForEntry,
+} from '~/ide/stores/actions';
import store from '~/ide/stores';
+import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
+import testAction from '../../helpers/vuex_action_helper';
describe('Multi-file store actions', () => {
beforeEach(() => {
@@ -14,12 +24,12 @@ describe('Multi-file store actions', () => {
describe('redirectToUrl', () => {
it('calls visitUrl', done => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(actions, 'visitUrl');
store
.dispatch('redirectToUrl', 'test')
.then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
+ expect(visitUrl).toHaveBeenCalledWith('test');
done();
})
@@ -191,9 +201,7 @@ describe('Multi-file store actions', () => {
})
.then(f => {
expect(f.tempFile).toBeTruthy();
- expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(
- 1,
- );
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
done();
})
@@ -292,6 +300,43 @@ describe('Multi-file store actions', () => {
});
});
+ describe('stageAllChanges', () => {
+ it('adds all files from changedFiles to stagedFiles', done => {
+ store.state.changedFiles.push(file(), file('new'));
+
+ testAction(
+ stageAllChanges,
+ null,
+ store.state,
+ [
+ { type: types.SET_LAST_COMMIT_MSG, payload: '' },
+ { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
+ { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('unstageAllChanges', () => {
+ it('removes all files from stagedFiles after unstaging', done => {
+ store.state.stagedFiles.push(file(), file('new'));
+
+ testAction(
+ unstageAllChanges,
+ null,
+ store.state,
+ [
+ { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path },
+ { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
describe('updateViewer', () => {
it('updates viewer state', done => {
store
@@ -303,4 +348,99 @@ describe('Multi-file store actions', () => {
.catch(done.fail);
});
});
+
+ describe('updateActivityBarView', () => {
+ it('commits UPDATE_ACTIVITY_BAR_VIEW', done => {
+ testAction(
+ updateActivityBarView,
+ 'test',
+ {},
+ [{ type: 'UPDATE_ACTIVITY_BAR_VIEW', payload: 'test' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setEmptyStateSvgs', () => {
+ it('commits setEmptyStateSvgs', done => {
+ testAction(
+ setEmptyStateSvgs,
+ 'svg',
+ {},
+ [{ type: 'SET_EMPTY_STATE_SVGS', payload: 'svg' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('updateTempFlagForEntry', () => {
+ it('commits UPDATE_TEMP_FLAG', done => {
+ const f = {
+ ...file(),
+ path: 'test',
+ tempFile: true,
+ };
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [],
+ done,
+ );
+ });
+
+ it('commits UPDATE_TEMP_FLAG and dispatches for parent', done => {
+ const parent = {
+ ...file(),
+ path: 'testing',
+ };
+ const f = {
+ ...file(),
+ path: 'test',
+ parentPath: 'testing',
+ };
+ store.state.entries[parent.path] = parent;
+ store.state.entries[f.path] = f;
+
+ testAction(
+ updateTempFlagForEntry,
+ { file: f, tempFile: false },
+ store.state,
+ [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }],
+ [{ type: 'updateTempFlagForEntry', payload: { file: parent, tempFile: false } }],
+ done,
+ );
+ });
+ });
+
+ describe('setCurrentBranchId', () => {
+ it('commits setCurrentBranchId', done => {
+ testAction(
+ setCurrentBranchId,
+ 'branchId',
+ {},
+ [{ type: 'SET_CURRENT_BRANCH', payload: 'branchId' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggleFileFinder', () => {
+ it('commits TOGGLE_FILE_FINDER', done => {
+ testAction(
+ toggleFileFinder,
+ true,
+ null,
+ [{ type: 'TOGGLE_FILE_FINDER', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 33733b97dff..4833ba3edfd 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -39,20 +39,6 @@ describe('IDE store getters', () => {
});
});
- describe('addedFiles', () => {
- it('returns a list of added files', () => {
- localState.openFiles.push(file());
- localState.changedFiles.push(file('added'));
- localState.changedFiles[0].changed = true;
- localState.changedFiles[0].tempFile = true;
-
- const modifiedFiles = getters.addedFiles(localState);
-
- expect(modifiedFiles.length).toBe(1);
- expect(modifiedFiles[0].name).toBe('added');
- });
- });
-
describe('currentMergeRequest', () => {
it('returns Current Merge Request', () => {
localState.currentProjectId = 'abcproject';
@@ -72,4 +58,107 @@ describe('IDE store getters', () => {
expect(getters.currentMergeRequest(localState)).toBeNull();
});
});
+
+ describe('allBlobs', () => {
+ beforeEach(() => {
+ Object.assign(localState.entries, {
+ index: { type: 'blob', name: 'index', lastOpenedAt: 0 },
+ app: { type: 'blob', name: 'blob', lastOpenedAt: 0 },
+ folder: { type: 'folder', name: 'folder', lastOpenedAt: 0 },
+ });
+ });
+
+ it('returns only blobs', () => {
+ expect(getters.allBlobs(localState).length).toBe(2);
+ });
+
+ it('returns list sorted by lastOpenedAt', () => {
+ localState.entries.app.lastOpenedAt = new Date().getTime();
+
+ expect(getters.allBlobs(localState)[0].name).toBe('blob');
+ });
+ });
+
+ describe('getChangesInFolder', () => {
+ it('returns length of changed files for a path', () => {
+ localState.changedFiles.push(
+ {
+ path: 'test/index',
+ name: 'index',
+ },
+ {
+ path: 'app/123',
+ name: '123',
+ },
+ );
+
+ expect(getters.getChangesInFolder(localState)('test')).toBe(1);
+ });
+
+ it('returns length of changed & staged files for a path', () => {
+ localState.changedFiles.push(
+ {
+ path: 'test/index',
+ name: 'index',
+ },
+ {
+ path: 'testing/123',
+ name: '123',
+ },
+ );
+
+ localState.stagedFiles.push(
+ {
+ path: 'test/123',
+ name: '123',
+ },
+ {
+ path: 'test/index',
+ name: 'index',
+ },
+ {
+ path: 'testing/12345',
+ name: '12345',
+ },
+ );
+
+ expect(getters.getChangesInFolder(localState)('test')).toBe(2);
+ });
+
+ it('returns length of changed & tempFiles files for a path', () => {
+ localState.changedFiles.push(
+ {
+ path: 'test/index',
+ name: 'index',
+ },
+ {
+ path: 'test/newfile',
+ name: 'newfile',
+ tempFile: true,
+ },
+ );
+
+ expect(getters.getChangesInFolder(localState)('test')).toBe(2);
+ });
+ });
+
+ describe('lastCommit', () => {
+ it('returns the last commit of the current branch on the current project', () => {
+ const commitTitle = 'Example commit title';
+ const localGetters = {
+ currentProject: {
+ branches: {
+ 'example-branch': {
+ commit: {
+ title: commitTitle,
+ },
+ },
+ },
+ },
+ };
+ localState.currentBranchId = 'example-branch';
+
+ expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 1946a0c547c..a2869ff378b 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -1,7 +1,7 @@
+import actions from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
-import * as urlUtils from '~/lib/utils/url_utility';
import eventHub from '~/ide/eventhub';
import * as consts from '~/ide/stores/modules/commit/constants';
import { resetStore, file } from 'spec/ide/helpers';
@@ -209,14 +209,14 @@ describe('IDE commit module actions', () => {
},
},
};
- store.state.changedFiles.push(f, {
+ store.state.stagedFiles.push(f, {
...file('changedFile2'),
changed: true,
});
- store.state.openFiles = store.state.changedFiles;
+ store.state.openFiles = store.state.stagedFiles;
- store.state.changedFiles.forEach(changedFile => {
- store.state.entries[changedFile.path] = changedFile;
+ store.state.stagedFiles.forEach(stagedFile => {
+ store.state.entries[stagedFile.path] = stagedFile;
});
});
@@ -248,19 +248,6 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('removes all changed files', done => {
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(store.state.changedFiles.length).toBe(0);
- })
- .then(done)
- .catch(done.fail);
- });
-
it('sets files commit data', done => {
store
.dispatch('commit/updateFilesAfterCommit', {
@@ -294,25 +281,10 @@ describe('IDE commit module actions', () => {
branch,
})
.then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith(
- `editor.update.model.content.${f.path}`,
- f.content,
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('pushes route to new branch if commitAction is new branch', done => {
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
-
- store
- .dispatch('commit/updateFilesAfterCommit', {
- data,
- branch,
- })
- .then(() => {
- expect(router.push).toHaveBeenCalledWith(`/project/abcproject/blob/master/${f.path}`);
+ expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, {
+ content: f.content,
+ changed: false,
+ });
})
.then(done)
.catch(done.fail);
@@ -320,8 +292,10 @@ describe('IDE commit module actions', () => {
});
describe('commitChanges', () => {
+ let visitUrl;
+
beforeEach(() => {
- spyOn(urlUtils, 'visitUrl');
+ visitUrl = spyOnDependency(actions, 'visitUrl');
document.body.innerHTML += '<div class="flash-container"></div>';
@@ -335,12 +309,22 @@ describe('IDE commit module actions', () => {
},
},
};
- store.state.changedFiles.push(file('changed'));
- store.state.changedFiles[0].active = true;
+
+ const f = {
+ ...file('changed'),
+ type: 'blob',
+ active: true,
+ };
+ store.state.stagedFiles.push(f);
+ store.state.changedFiles = [
+ {
+ ...f,
+ },
+ ];
store.state.openFiles = store.state.changedFiles;
- store.state.openFiles.forEach(f => {
- store.state.entries[f.path] = f;
+ store.state.openFiles.forEach(localF => {
+ store.state.entries[localF.path] = localF;
});
store.state.commit.commitAction = '2';
@@ -392,14 +376,12 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('pushes router to new route', done => {
+ it('sets last Commit Msg', done => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(router.push).toHaveBeenCalledWith(
- `/project/${store.state.currentProjectId}/blob/${
- store.getters['commit/newBranchName']
- }/changed`,
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="webUrl/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
);
done();
@@ -407,12 +389,12 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('sets last Commit Msg', done => {
+ it('adds commit data to files', done => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.lastCommitMsg).toBe(
- 'Your changes have been committed. Commit <a href="webUrl/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ expect(store.state.entries[store.state.openFiles[0].path].lastCommit.message).toBe(
+ 'test message',
);
done();
@@ -420,24 +402,23 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
- it('adds commit data to changed files', done => {
+ it('resets stores commit actions', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.openFiles[0].lastCommit.message).toBe('test message');
-
- done();
+ expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH);
})
+ .then(done)
.catch(done.fail);
});
- it('resets stores commit actions', done => {
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
-
+ it('removes all staged files', done => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH);
+ expect(store.state.stagedFiles.length).toBe(0);
})
.then(done)
.catch(done.fail);
@@ -452,7 +433,7 @@ describe('IDE commit module actions', () => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(
+ expect(visitUrl).toHaveBeenCalledWith(
`webUrl/merge_requests/new?merge_request[source_branch]=${
store.getters['commit/newBranchName']
}&merge_request[target_branch]=master`,
@@ -471,7 +452,7 @@ describe('IDE commit module actions', () => {
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.changedFiles.length).toBe(0);
+ expect(store.state.stagedFiles.length).toBe(0);
done();
})
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index e396284ec2c..55580f046ad 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -34,17 +34,17 @@ describe('IDE commit module getters', () => {
discardDraftButtonDisabled: false,
};
const rootState = {
- changedFiles: ['a'],
+ stagedFiles: ['a'],
};
- it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ it('returns false when discardDraftButtonDisabled is false & stagedFiles is not empty', () => {
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
).toBeFalsy();
});
- it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => {
- rootState.changedFiles.length = 0;
+ it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => {
+ rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
@@ -61,7 +61,7 @@ describe('IDE commit module getters', () => {
it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
localGetters.discardDraftButtonDisabled = false;
- rootState.changedFiles.length = 0;
+ rootState.stagedFiles.length = 0;
expect(
getters.commitButtonDisabled(state, localGetters, rootState),
diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
new file mode 100644
index 00000000000..85fbcf8084b
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
@@ -0,0 +1,289 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import actions, {
+ requestLatestPipeline,
+ receiveLatestPipelineError,
+ receiveLatestPipelineSuccess,
+ fetchLatestPipeline,
+ requestJobs,
+ receiveJobsError,
+ receiveJobsSuccess,
+ fetchJobs,
+} from '~/ide/stores/modules/pipelines/actions';
+import state from '~/ide/stores/modules/pipelines/state';
+import * as types from '~/ide/stores/modules/pipelines/mutation_types';
+import testAction from '../../../../helpers/vuex_action_helper';
+import { pipelines, jobs } from '../../../mock_data';
+
+describe('IDE pipelines actions', () => {
+ let mockedState;
+ let mock;
+
+ beforeEach(() => {
+ mockedState = state();
+ mock = new MockAdapter(axios);
+
+ gon.api_version = 'v4';
+ mockedState.currentProjectId = 'test/project';
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('requestLatestPipeline', () => {
+ it('commits request', done => {
+ testAction(
+ requestLatestPipeline,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_LATEST_PIPELINE }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveLatestPipelineError', () => {
+ it('commits error', done => {
+ testAction(
+ receiveLatestPipelineError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
+ [],
+ done,
+ );
+ });
+
+ it('creates flash message', () => {
+ const flashSpy = spyOnDependency(actions, 'flash');
+
+ receiveLatestPipelineError({ commit() {} });
+
+ expect(flashSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('receiveLatestPipelineSuccess', () => {
+ it('commits pipeline', done => {
+ testAction(
+ receiveLatestPipelineSuccess,
+ pipelines[0],
+ mockedState,
+ [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchLatestPipeline', () => {
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines);
+ });
+
+ it('dispatches request', done => {
+ testAction(
+ fetchLatestPipeline,
+ '123',
+ mockedState,
+ [],
+ [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }],
+ done,
+ );
+ });
+
+ it('dispatches success with latest pipeline', done => {
+ testAction(
+ fetchLatestPipeline,
+ '123',
+ mockedState,
+ [],
+ [
+ { type: 'requestLatestPipeline' },
+ { type: 'receiveLatestPipelineSuccess', payload: pipelines[0] },
+ ],
+ done,
+ );
+ });
+
+ it('calls axios with correct params', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchLatestPipeline({ dispatch() {}, rootState: state }, '123');
+
+ expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
+ params: {
+ sha: '123',
+ per_page: '1',
+ },
+ });
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ fetchLatestPipeline,
+ '123',
+ mockedState,
+ [],
+ [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('requestJobs', () => {
+ it('commits request', done => {
+ testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done);
+ });
+ });
+
+ describe('receiveJobsError', () => {
+ it('commits error', done => {
+ testAction(
+ receiveJobsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_JOBS_ERROR }],
+ [],
+ done,
+ );
+ });
+
+ it('creates flash message', () => {
+ const flashSpy = spyOnDependency(actions, 'flash');
+
+ receiveJobsError({ commit() {} });
+
+ expect(flashSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('receiveJobsSuccess', () => {
+ it('commits jobs', done => {
+ testAction(
+ receiveJobsSuccess,
+ jobs,
+ mockedState,
+ [{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchJobs', () => {
+ let page = '';
+
+ beforeEach(() => {
+ mockedState.latestPipeline = pipelines[0];
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [
+ 200,
+ jobs,
+ {
+ 'x-next-page': page,
+ },
+ ]);
+ });
+
+ it('dispatches request', done => {
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }],
+ done,
+ );
+ });
+
+ it('dispatches success with latest pipeline', done => {
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }],
+ done,
+ );
+ });
+
+ it('dispatches twice for both pages', done => {
+ page = '2';
+
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestJobs' },
+ { type: 'receiveJobsSuccess', payload: jobs },
+ { type: 'fetchJobs', payload: '2' },
+ { type: 'requestJobs' },
+ { type: 'receiveJobsSuccess', payload: jobs },
+ ],
+ done,
+ );
+ });
+
+ it('calls axios with correct URL', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
+
+ expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
+ params: { page: '1' },
+ });
+ });
+
+ it('calls axios with page next page', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
+
+ expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
+ params: { page: '1' },
+ });
+
+ page = '2';
+
+ fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page);
+
+ expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
+ params: { page: '2' },
+ });
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ fetchJobs,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestJobs' }, { type: 'receiveJobsError' }],
+ done,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
new file mode 100644
index 00000000000..b2a7e8a9025
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
@@ -0,0 +1,71 @@
+import * as getters from '~/ide/stores/modules/pipelines/getters';
+import state from '~/ide/stores/modules/pipelines/state';
+
+describe('IDE pipeline getters', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('hasLatestPipeline', () => {
+ it('returns false when loading is true', () => {
+ mockedState.isLoadingPipeline = true;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(false);
+ });
+
+ it('returns false when pipelines is null', () => {
+ mockedState.latestPipeline = null;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(false);
+ });
+
+ it('returns false when loading is true & pipelines is null', () => {
+ mockedState.latestPipeline = null;
+ mockedState.isLoadingPipeline = true;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(false);
+ });
+
+ it('returns true when loading is false & pipelines is an object', () => {
+ mockedState.latestPipeline = {
+ id: 1,
+ };
+ mockedState.isLoadingPipeline = false;
+
+ expect(getters.hasLatestPipeline(mockedState)).toBe(true);
+ });
+ });
+
+ describe('failedJobs', () => {
+ it('returns array of failed jobs', () => {
+ mockedState.stages = [
+ {
+ title: 'test',
+ jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }],
+ },
+ {
+ title: 'build',
+ jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }],
+ },
+ ];
+
+ expect(getters.failedJobs(mockedState).length).toBe(3);
+ expect(getters.failedJobs(mockedState)).toEqual([
+ {
+ id: 1,
+ status: jasmine.anything(),
+ },
+ {
+ id: 3,
+ status: jasmine.anything(),
+ },
+ {
+ id: 4,
+ status: jasmine.anything(),
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
new file mode 100644
index 00000000000..8262e916243
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
@@ -0,0 +1,120 @@
+import mutations from '~/ide/stores/modules/pipelines/mutations';
+import state from '~/ide/stores/modules/pipelines/state';
+import * as types from '~/ide/stores/modules/pipelines/mutation_types';
+import { pipelines, jobs } from '../../../mock_data';
+
+describe('IDE pipelines mutations', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe(types.REQUEST_LATEST_PIPELINE, () => {
+ it('sets loading to true', () => {
+ mutations[types.REQUEST_LATEST_PIPELINE](mockedState);
+
+ expect(mockedState.isLoadingPipeline).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_LASTEST_PIPELINE_ERROR, () => {
+ it('sets loading to false', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_ERROR](mockedState);
+
+ expect(mockedState.isLoadingPipeline).toBe(false);
+ });
+ });
+
+ describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
+ it('sets loading to false on success', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+
+ expect(mockedState.isLoadingPipeline).toBe(false);
+ });
+
+ it('sets latestPipeline', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+
+ expect(mockedState.latestPipeline).toEqual({
+ id: pipelines[0].id,
+ status: pipelines[0].status,
+ });
+ });
+
+ it('does not set latest pipeline if pipeline is null', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
+
+ expect(mockedState.latestPipeline).toEqual(null);
+ });
+ });
+
+ describe(types.REQUEST_JOBS, () => {
+ it('sets jobs loading to true', () => {
+ mutations[types.REQUEST_JOBS](mockedState);
+
+ expect(mockedState.isLoadingJobs).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_JOBS_ERROR, () => {
+ it('sets jobs loading to false', () => {
+ mutations[types.RECEIVE_JOBS_ERROR](mockedState);
+
+ expect(mockedState.isLoadingJobs).toBe(false);
+ });
+ });
+
+ describe(types.RECEIVE_JOBS_SUCCESS, () => {
+ it('sets jobs loading to false on success', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+
+ expect(mockedState.isLoadingJobs).toBe(false);
+ });
+
+ it('sets stages', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+
+ expect(mockedState.stages.length).toBe(2);
+ expect(mockedState.stages).toEqual([
+ {
+ title: 'test',
+ jobs: jasmine.anything(),
+ },
+ {
+ title: 'build',
+ jobs: jasmine.anything(),
+ },
+ ]);
+ });
+
+ it('sets jobs in stages', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+
+ expect(mockedState.stages[0].jobs.length).toBe(3);
+ expect(mockedState.stages[1].jobs.length).toBe(1);
+ expect(mockedState.stages).toEqual([
+ {
+ title: jasmine.anything(),
+ jobs: jobs.filter(job => job.stage === 'test').map(job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ stage: job.stage,
+ duration: job.duration,
+ })),
+ },
+ {
+ title: jasmine.anything(),
+ jobs: jobs.filter(job => job.stage === 'build').map(job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ stage: job.stage,
+ duration: job.duration,
+ })),
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js
index a7167537ef2..f2f1f2a9a2e 100644
--- a/spec/javascripts/ide/stores/mutations/branch_spec.js
+++ b/spec/javascripts/ide/stores/mutations/branch_spec.js
@@ -15,4 +15,62 @@ describe('Multi-file store branch mutations', () => {
expect(localState.currentBranchId).toBe('master');
});
});
+
+ describe('SET_BRANCH_COMMIT', () => {
+ it('sets the last commit on current project', () => {
+ localState.projects = {
+ Example: {
+ branches: {
+ master: {},
+ },
+ },
+ };
+
+ mutations.SET_BRANCH_COMMIT(localState, {
+ projectId: 'Example',
+ branchId: 'master',
+ commit: {
+ title: 'Example commit',
+ },
+ });
+
+ expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
+ });
+ });
+
+ describe('SET_LAST_COMMIT_PIPELINE', () => {
+ it('sets the pipeline for the last commit on current project', () => {
+ localState.projects = {
+ Example: {
+ branches: {
+ master: {
+ commit: {},
+ },
+ },
+ },
+ };
+
+ mutations.SET_LAST_COMMIT_PIPELINE(localState, {
+ projectId: 'Example',
+ branchId: 'master',
+ pipeline: {
+ id: '50',
+ details: {
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
+ },
+ },
+ });
+
+ expect(localState.projects.Example.branches.master.commit.pipeline.id).toBe('50');
+ expect(localState.projects.Example.branches.master.commit.pipeline.details.status.text).toBe(
+ 'passed',
+ );
+ expect(localState.projects.Example.branches.master.commit.pipeline.details.status.icon).toBe(
+ 'status_passed',
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index bf9d5166d0a..e83961fcedc 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -8,7 +8,10 @@ describe('IDE store file mutations', () => {
beforeEach(() => {
localState = state();
- localFile = file();
+ localFile = {
+ ...file(),
+ type: 'blob',
+ };
localState.entries[localFile.path] = localFile;
});
@@ -183,6 +186,49 @@ describe('IDE store file mutations', () => {
});
});
+ describe('STAGE_CHANGE', () => {
+ it('adds file into stagedFiles array', () => {
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ expect(localState.stagedFiles.length).toBe(1);
+ expect(localState.stagedFiles[0]).toEqual(localFile);
+ });
+
+ it('updates stagedFile if it is already staged', () => {
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ localFile.raw = 'testing 123';
+
+ mutations.STAGE_CHANGE(localState, localFile.path);
+
+ expect(localState.stagedFiles.length).toBe(1);
+ expect(localState.stagedFiles[0].raw).toEqual('testing 123');
+ });
+ });
+
+ describe('UNSTAGE_CHANGE', () => {
+ let f;
+
+ beforeEach(() => {
+ f = {
+ ...file(),
+ type: 'blob',
+ staged: true,
+ };
+
+ localState.stagedFiles.push(f);
+ localState.changedFiles.push(f);
+ localState.entries[f.path] = f;
+ });
+
+ it('removes from stagedFiles array', () => {
+ mutations.UNSTAGE_CHANGE(localState, f.path);
+
+ expect(localState.stagedFiles.length).toBe(0);
+ expect(localState.changedFiles.length).toBe(1);
+ });
+ });
+
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
mutations.TOGGLE_FILE_CHANGED(localState, {
@@ -221,41 +267,23 @@ describe('IDE store file mutations', () => {
it('adds file into openFiles as pending', () => {
mutations.ADD_PENDING_TAB(localState, { file: localFile });
- expect(localState.openFiles.length).toBe(2);
- expect(localState.openFiles[1].pending).toBe(true);
- expect(localState.openFiles[1].key).toBe(`pending-${localFile.key}`);
- });
-
- it('updates open file to pending', () => {
- mutations.ADD_PENDING_TAB(localState, { file: localState.openFiles[0] });
-
expect(localState.openFiles.length).toBe(1);
+ expect(localState.openFiles[0].pending).toBe(true);
+ expect(localState.openFiles[0].key).toBe(`pending-${localFile.key}`);
});
- it('updates pending open file to active', () => {
- localState.openFiles.push({
- ...localFile,
- pending: true,
- });
+ it('only allows 1 open pending file', () => {
+ const newFile = file('test');
+ localState.entries[newFile.path] = newFile;
mutations.ADD_PENDING_TAB(localState, { file: localFile });
- expect(localState.openFiles[1].pending).toBe(true);
- expect(localState.openFiles[1].active).toBe(true);
- });
-
- it('sets all openFiles to not active', () => {
- mutations.ADD_PENDING_TAB(localState, { file: localFile });
+ expect(localState.openFiles.length).toBe(1);
- expect(localState.openFiles.length).toBe(2);
+ mutations.ADD_PENDING_TAB(localState, { file: file('test') });
- localState.openFiles.forEach(f => {
- if (f.pending) {
- expect(f.active).toBe(true);
- } else {
- expect(f.active).toBe(false);
- }
- });
+ expect(localState.openFiles.length).toBe(1);
+ expect(localState.openFiles[0].name).toBe('test');
});
});
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 38162a470ad..972713c5ad2 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -69,6 +69,16 @@ describe('Multi-file store mutations', () => {
});
});
+ describe('CLEAR_STAGED_CHANGES', () => {
+ it('clears stagedFiles array', () => {
+ localState.stagedFiles.push('a');
+
+ mutations.CLEAR_STAGED_CHANGES(localState);
+
+ expect(localState.stagedFiles.length).toBe(0);
+ });
+ });
+
describe('UPDATE_VIEWER', () => {
it('sets viewer state', () => {
mutations.UPDATE_VIEWER(localState, 'diff');
@@ -76,4 +86,66 @@ describe('Multi-file store mutations', () => {
expect(localState.viewer).toBe('diff');
});
});
+
+ describe('UPDATE_ACTIVITY_BAR_VIEW', () => {
+ it('updates currentActivityBar', () => {
+ mutations.UPDATE_ACTIVITY_BAR_VIEW(localState, 'test');
+
+ expect(localState.currentActivityView).toBe('test');
+ });
+ });
+
+ describe('SET_EMPTY_STATE_SVGS', () => {
+ it('updates empty state SVGs', () => {
+ mutations.SET_EMPTY_STATE_SVGS(localState, {
+ emptyStateSvgPath: 'emptyState',
+ noChangesStateSvgPath: 'noChanges',
+ committedStateSvgPath: 'commited',
+ });
+
+ expect(localState.emptyStateSvgPath).toBe('emptyState');
+ expect(localState.noChangesStateSvgPath).toBe('noChanges');
+ expect(localState.committedStateSvgPath).toBe('commited');
+ });
+ });
+
+ describe('UPDATE_TEMP_FLAG', () => {
+ beforeEach(() => {
+ localState.entries.test = {
+ ...file(),
+ tempFile: true,
+ changed: true,
+ };
+ });
+
+ it('updates tempFile flag', () => {
+ mutations.UPDATE_TEMP_FLAG(localState, { path: 'test', tempFile: false });
+
+ expect(localState.entries.test.tempFile).toBe(false);
+ });
+
+ it('updates changed flag', () => {
+ mutations.UPDATE_TEMP_FLAG(localState, { path: 'test', tempFile: false });
+
+ expect(localState.entries.test.changed).toBe(false);
+ });
+ });
+
+ describe('TOGGLE_FILE_FINDER', () => {
+ it('updates fileFindVisible', () => {
+ mutations.TOGGLE_FILE_FINDER(localState, true);
+
+ expect(localState.fileFindVisible).toBe(true);
+ });
+ });
+
+ describe('BURST_UNUSED_SEAL', () => {
+ it('updates unusedSeal', () => {
+ expect(localState.unusedSeal).toBe(true);
+
+ mutations.BURST_UNUSED_SEAL(localState);
+
+ expect(localState.unusedSeal).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index d5a87b5ce20..bf1f0c822fe 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -2,7 +2,6 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import '~/behaviors/markdown/render_gfm';
-import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
@@ -174,7 +173,7 @@ describe('Issuable output', () => {
});
it('does not redirect if issue has not moved', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -187,16 +186,13 @@ describe('Issuable output', () => {
vm.updateIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).not.toHaveBeenCalled();
-
+ expect(visitUrl).not.toHaveBeenCalled();
done();
});
});
it('redirects if returned web_url has changed', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -209,10 +205,7 @@ describe('Issuable output', () => {
vm.updateIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).toHaveBeenCalledWith('/testing-issue-move');
-
+ expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
done();
});
});
@@ -340,7 +333,7 @@ describe('Issuable output', () => {
describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
data: {
@@ -352,16 +345,13 @@ describe('Issuable output', () => {
vm.deleteIssuable();
setTimeout(() => {
- expect(
- urlUtils.visitUrl,
- ).toHaveBeenCalledWith('/test');
-
+ expect(visitUrl).toHaveBeenCalledWith('/test');
done();
});
});
it('stops polling when deleting', (done) => {
- spyOn(urlUtils, 'visitUrl');
+ spyOnDependency(issuableApp, 'visitUrl');
spyOn(vm.poll, 'stop').and.callThrough();
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
@@ -377,7 +367,6 @@ describe('Issuable output', () => {
expect(
vm.poll.stop,
).toHaveBeenCalledWith();
-
done();
});
});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index d96151a8a3a..889c8545faa 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import descriptionComponent from '~/issue_show/components/description.vue';
-import * as taskList from '~/task_list';
+import Description from '~/issue_show/components/description.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Description component', () => {
@@ -17,7 +16,7 @@ describe('Description component', () => {
};
beforeEach(() => {
- DescriptionComponent = Vue.extend(descriptionComponent);
+ DescriptionComponent = Vue.extend(Description);
if (!document.querySelector('.issuable-meta')) {
const metaData = document.createElement('div');
@@ -82,18 +81,20 @@ describe('Description component', () => {
});
describe('TaskList', () => {
+ let TaskList;
+
beforeEach(() => {
vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
issuableType: 'issuableType',
}));
- spyOn(taskList, 'default');
+ TaskList = spyOnDependency(Description, 'TaskList');
});
it('re-inits the TaskList when description changed', (done) => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).toHaveBeenCalled();
+ expect(TaskList).toHaveBeenCalled();
done();
});
});
@@ -103,7 +104,7 @@ describe('Description component', () => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).not.toHaveBeenCalled();
+ expect(TaskList).not.toHaveBeenCalled();
done();
});
});
@@ -112,7 +113,7 @@ describe('Description component', () => {
vm.descriptionHtml = 'changed';
setTimeout(() => {
- expect(taskList.default).toHaveBeenCalledWith({
+ expect(TaskList).toHaveBeenCalledWith({
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index f37426a72d4..047ecab27db 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -92,6 +92,7 @@ describe('Issue', function() {
function mockCanCreateBranch(canCreateBranch) {
mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
can_create_branch: canCreateBranch,
+ suggested_branch_name: 'foo-99',
});
}
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index c6bbacf237a..da00b615c9b 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility';
import Job from '~/job';
import '~/breakpoints';
@@ -22,7 +21,7 @@ describe('Job', () => {
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
- spyOn(urlUtils, 'visitUrl');
+ spyOnDependency(Job, 'visitUrl');
response = {};
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index 0961605ce5c..4f861c39d3f 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -36,14 +36,28 @@ describe('Job details header', () => {
},
isLoading: false,
};
-
- vm = mountComponent(HeaderComponent, props);
});
afterEach(() => {
vm.$destroy();
});
+ describe('job reason', () => {
+ it('should not render the reason when reason is absent', () => {
+ vm = mountComponent(HeaderComponent, props);
+
+ expect(vm.shouldRenderReason).toBe(false);
+ });
+
+ it('should render the reason when reason is present', () => {
+ props.job.callout_message = 'There is an unknown failure, please try again';
+
+ vm = mountComponent(HeaderComponent, props);
+
+ expect(vm.shouldRenderReason).toBe(true);
+ });
+ });
+
describe('triggered job', () => {
beforeEach(() => {
vm = mountComponent(HeaderComponent, props);
@@ -51,14 +65,17 @@ describe('Job details header', () => {
it('should render provided job information', () => {
expect(
- vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.header-main-content')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual('failed Job #123 triggered 3 weeks ago by Foo');
});
it('should render new issue link', () => {
- expect(
- vm.$el.querySelector('.js-new-issue').getAttribute('href'),
- ).toEqual(props.job.new_issue_path);
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ props.job.new_issue_path,
+ );
});
});
@@ -68,7 +85,10 @@ describe('Job details header', () => {
vm = mountComponent(HeaderComponent, props);
expect(
- vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ vm.$el
+ .querySelector('.header-main-content')
+ .textContent.replace(/\s+/g, ' ')
+ .trim(),
).toEqual('failed Job #123 created 3 weeks ago by Foo');
});
});
diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js
index 602dae514b1..9c4454252ce 100644
--- a/spec/javascripts/jobs/sidebar_details_block_spec.js
+++ b/spec/javascripts/jobs/sidebar_details_block_spec.js
@@ -31,10 +31,25 @@ describe('Sidebar details block', () => {
});
});
+ describe("when user can't retry", () => {
+ it('should not render a retry button', () => {
+ vm = new SidebarComponent({
+ propsData: {
+ job: {},
+ canUserRetry: false,
+ isLoading: true,
+ },
+ }).$mount();
+
+ expect(vm.$el.querySelector('.js-retry-job')).toBeNull();
+ });
+ });
+
beforeEach(() => {
vm = new SidebarComponent({
propsData: {
job,
+ canUserRetry: true,
isLoading: false,
},
}).$mount();
@@ -42,7 +57,9 @@ describe('Sidebar details block', () => {
describe('actions', () => {
it('should render link to new issue', () => {
- expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path);
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ job.new_issue_path,
+ );
expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
});
@@ -57,43 +74,35 @@ describe('Sidebar details block', () => {
describe('information', () => {
it('should render merge request link', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-mr')),
- ).toEqual('Merge Request: !2');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-mr'))).toEqual('Merge Request: !2');
- expect(
- vm.$el.querySelector('.js-job-mr a').getAttribute('href'),
- ).toEqual(job.merge_request.path);
+ expect(vm.$el.querySelector('.js-job-mr a').getAttribute('href')).toEqual(
+ job.merge_request.path,
+ );
});
it('should render job duration', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-duration')),
- ).toEqual('Duration: 6 seconds');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-duration'))).toEqual(
+ 'Duration: 6 seconds',
+ );
});
it('should render erased date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-erased')),
- ).toEqual('Erased: 3 weeks ago');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-erased'))).toEqual('Erased: 3 weeks ago');
});
it('should render finished date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-finished')),
- ).toEqual('Finished: 3 weeks ago');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-finished'))).toEqual(
+ 'Finished: 3 weeks ago',
+ );
});
it('should render queued date', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-queued')),
- ).toEqual('Queued: 9 seconds');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-queued'))).toEqual('Queued: 9 seconds');
});
it('should render runner ID', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-runner')),
- ).toEqual('Runner: #1');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: local ci runner (#1)');
});
it('should render timeout information', () => {
@@ -103,15 +112,11 @@ describe('Sidebar details block', () => {
});
it('should render coverage', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
- ).toEqual('Coverage: 20%');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-coverage'))).toEqual('Coverage: 20%');
});
it('should render tags', () => {
- expect(
- trimWhitespace(vm.$el.querySelector('.js-job-tags')),
- ).toEqual('Tags: tag');
+ expect(trimWhitespace(vm.$el.querySelector('.js-job-tags'))).toEqual('Tags: tag');
});
});
});
diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js
index c484213df8e..81a39a97a84 100644
--- a/spec/javascripts/lib/utils/csrf_token_spec.js
+++ b/spec/javascripts/lib/utils/csrf_token_spec.js
@@ -1,6 +1,6 @@
import csrf from '~/lib/utils/csrf';
-describe('csrf', () => {
+describe('csrf', function () {
beforeEach(() => {
this.tokenKey = 'X-CSRF-Token';
this.token = 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ==';
diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js
index 75addfcc833..a7eff419fba 100644
--- a/spec/javascripts/lib/utils/image_utility_spec.js
+++ b/spec/javascripts/lib/utils/image_utility_spec.js
@@ -1,4 +1,4 @@
-import * as imageUtility from '~/lib/utils/image_utility';
+import { isImageLoaded } from '~/lib/utils/image_utility';
describe('imageUtility', () => {
describe('isImageLoaded', () => {
@@ -8,7 +8,7 @@ describe('imageUtility', () => {
naturalHeight: 100,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ expect(isImageLoaded(element)).toEqual(false);
});
it('should return false when naturalHeight = 0', () => {
@@ -17,7 +17,7 @@ describe('imageUtility', () => {
naturalHeight: 0,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(false);
+ expect(isImageLoaded(element)).toEqual(false);
});
it('should return true when image.complete and naturalHeight != 0', () => {
@@ -26,7 +26,7 @@ describe('imageUtility', () => {
naturalHeight: 100,
};
- expect(imageUtility.isImageLoaded(element)).toEqual(true);
+ expect(isImageLoaded(element)).toEqual(true);
});
});
});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index ae00fb76714..eab5c24406a 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -75,6 +75,14 @@ describe('text_utility', () => {
'This is a text with html .',
);
});
+
+ it('passes through with null string input', () => {
+ expect(textUtils.stripHtml(null, ' ')).toEqual(null);
+ });
+
+ it('passes through with undefined string input', () => {
+ expect(textUtils.stripHtml(undefined, ' ')).toEqual(undefined);
+ });
});
describe('convertToCamelCase', () => {
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 79c8cf0ba32..3dbd9756cd2 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -3,7 +3,6 @@
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
@@ -356,7 +355,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -372,7 +371,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -385,7 +384,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
@@ -422,7 +421,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
@@ -439,7 +438,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function (done) {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
setTimeout(() => {
@@ -451,7 +450,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOnDependency(MergeRequestTabs, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index 2d474e9092f..19278312b6d 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -22,15 +22,20 @@ const defaultValuesComponent = {
graphHeightOffset: 120,
showFlagContent: true,
realPixelRatio: 1,
- timeSeries: [{
- values: [{
- time: new Date('2017-06-04T18:17:33.501Z'),
- value: '1.49609375',
- }],
- }],
+ timeSeries: [
+ {
+ values: [
+ {
+ time: new Date('2017-06-04T18:17:33.501Z'),
+ value: '1.49609375',
+ },
+ ],
+ },
+ ],
unitOfDisplay: 'ms',
currentDataIndex: 0,
legendTitle: 'Average',
+ currentCoordinates: [],
};
const deploymentFlagData = {
@@ -113,7 +118,7 @@ describe('GraphFlag', () => {
});
it('formatDate', () => {
- expect(component.formatDate).toEqual('Sun, Jun 4');
+ expect(component.formatDate).toEqual('04 Jun 2017, ');
});
it('cursorStyle', () => {
diff --git a/spec/javascripts/monitoring/graph/track_line_spec.js b/spec/javascripts/monitoring/graph/track_line_spec.js
index 45106830a67..27602a861eb 100644
--- a/spec/javascripts/monitoring/graph/track_line_spec.js
+++ b/spec/javascripts/monitoring/graph/track_line_spec.js
@@ -39,14 +39,14 @@ describe('TrackLine component', () => {
const svgEl = vm.$el.querySelector('svg');
const lineEl = vm.$el.querySelector('svg line');
- expect(svgEl.getAttribute('width')).toEqual('15');
- expect(svgEl.getAttribute('height')).toEqual('6');
+ expect(svgEl.getAttribute('width')).toEqual('16');
+ expect(svgEl.getAttribute('height')).toEqual('8');
expect(lineEl.getAttribute('stroke-width')).toEqual('4');
expect(lineEl.getAttribute('x1')).toEqual('0');
- expect(lineEl.getAttribute('x2')).toEqual('15');
- expect(lineEl.getAttribute('y1')).toEqual('2');
- expect(lineEl.getAttribute('y2')).toEqual('2');
+ expect(lineEl.getAttribute('x2')).toEqual('16');
+ expect(lineEl.getAttribute('y1')).toEqual('4');
+ expect(lineEl.getAttribute('y2')).toEqual('4');
});
});
});
diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
index c83bd19345f..2515e2ad897 100644
--- a/spec/javascripts/monitoring/graph_path_spec.js
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -23,6 +23,7 @@ describe('Monitoring Paths', () => {
generatedAreaPath: firstTimeSeries.areaPath,
lineColor: firstTimeSeries.lineColor,
areaColor: firstTimeSeries.areaColor,
+ showDot: false,
});
const metricArea = component.$el.querySelector('.metric-area');
const metricLine = component.$el.querySelector('.metric-line');
@@ -40,6 +41,7 @@ describe('Monitoring Paths', () => {
generatedAreaPath: firstTimeSeries.areaPath,
lineColor: firstTimeSeries.lineColor,
areaColor: firstTimeSeries.areaColor,
+ showDot: false,
});
component.lineStyle = 'dashed';
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 1213c80ba3a..220228e5c08 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -30,7 +30,6 @@ describe('Graph', () => {
it('has a title', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -46,7 +45,6 @@ describe('Graph', () => {
it('axisTransform translates an element Y position depending of its height', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -62,7 +60,6 @@ describe('Graph', () => {
it('outerViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -79,7 +76,6 @@ describe('Graph', () => {
it('sends an event to the eventhub when it has finished resizing', done => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -97,7 +93,6 @@ describe('Graph', () => {
it('has a title for the y-axis and the chart legend that comes from the backend', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
tagsPath,
@@ -111,7 +106,6 @@ describe('Graph', () => {
it('sets the currentData object based on the hovered data index', () => {
const component = createComponent({
graphData: convertedMetrics[1],
- classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
graphIdentifier: 0,
@@ -125,6 +119,5 @@ describe('Graph', () => {
component.positionFlag();
expect(component.currentData).toBe(component.timeSeries[0].values[10]);
- expect(component.currentDataIndex).toEqual(10);
});
});
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index 88aa7659275..08d54946787 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -1,7 +1,7 @@
import MonitoringStore from '~/monitoring/stores/monitoring_store';
import MonitoringMock, { deploymentData } from './mock_data';
-describe('MonitoringStore', () => {
+describe('MonitoringStore', function () {
this.store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index ec56ab0e2f0..648fb3e9bd3 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -3,7 +3,6 @@ import $ from 'jquery';
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
@@ -222,7 +221,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('sets target when hash matches', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(hash);
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue(hash);
Notes.updateNoteTargetSelector($note);
@@ -231,7 +230,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('unsets target when hash does not match', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist');
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue('note_doesnotexist');
Notes.updateNoteTargetSelector($note);
@@ -239,7 +238,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('unsets target when there is not a hash fragment anymore', () => {
- spyOn(urlUtils, 'getLocationHash').and.returnValue(null);
+ spyOnDependency(Notes, 'getLocationHash').and.returnValue(null);
Notes.updateNoteTargetSelector($note);
@@ -975,7 +974,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
).toBeFalsy();
expect(
$tempNoteHeader
- .find('.hidden-xs')
+ .find('.d-none.d-sm-block')
.text()
.trim(),
).toEqual(currentUserFullname);
@@ -1021,7 +1020,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const $tempNoteHeader = $tempNote.find('.note-header');
expect(
$tempNoteHeader
- .find('.hidden-xs')
+ .find('.d-none.d-sm-block')
.text()
.trim(),
).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index b09494f0b77..04f2e7ef4f9 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -1,15 +1,25 @@
-/* global fixture */
+import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as utils from '~/lib/utils/url_utility';
import Pager from '~/pager';
describe('pager', () => {
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
describe('init', () => {
const originalHref = window.location.href;
beforeEach(() => {
setFixtures('<div class="content_list"></div><div class="loading"></div>');
+ spyOn($.fn, 'endlessScroll').and.stub();
});
afterEach(() => {
@@ -25,7 +35,7 @@ describe('pager', () => {
it('should use current url if data-href attribute not provided', () => {
const href = `${gl.TEST_HOST}/some_list`;
- spyOn(utils, 'removeParams').and.returnValue(href);
+ spyOnDependency(Pager, 'removeParams').and.returnValue(href);
Pager.init();
expect(Pager.url).toBe(href);
});
@@ -39,42 +49,37 @@ describe('pager', () => {
it('keeps extra query parameters from url', () => {
window.history.replaceState({}, null, '?filter=test&offset=100');
const href = `${gl.TEST_HOST}/some_list?filter=test`;
- spyOn(utils, 'removeParams').and.returnValue(href);
+ const removeParams = spyOnDependency(Pager, 'removeParams').and.returnValue(href);
Pager.init();
- expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+ expect(removeParams).toHaveBeenCalledWith(['limit', 'offset']);
expect(Pager.url).toEqual(href);
});
});
describe('getOld', () => {
const urlRegex = /(.*)some_list(.*)$/;
- let mock;
function mockSuccess() {
- mock.onGet(urlRegex).reply(200, {
+ axiosMock.onGet(urlRegex).reply(200, {
count: 0,
html: '',
});
}
function mockError() {
- mock.onGet(urlRegex).networkError();
+ axiosMock.onGet(urlRegex).networkError();
}
beforeEach(() => {
- setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+ setFixtures(
+ '<div class="content_list" data-href="/some_list"></div><div class="loading"></div>',
+ );
spyOn(axios, 'get').and.callThrough();
- mock = new MockAdapter(axios);
-
Pager.init();
});
- afterEach(() => {
- mock.restore();
- });
-
- it('shows loader while loading next page', (done) => {
+ it('shows loader while loading next page', done => {
mockSuccess();
spyOn(Pager.loading, 'show');
@@ -87,7 +92,7 @@ describe('pager', () => {
});
});
- it('hides loader on success', (done) => {
+ it('hides loader on success', done => {
mockSuccess();
spyOn(Pager.loading, 'hide');
@@ -100,7 +105,7 @@ describe('pager', () => {
});
});
- it('hides loader on error', (done) => {
+ it('hides loader on error', done => {
mockError();
spyOn(Pager.loading, 'hide');
@@ -113,7 +118,7 @@ describe('pager', () => {
});
});
- it('sends request to url with offset and limit params', (done) => {
+ it('sends request to url with offset and limit params', done => {
Pager.offset = 100;
Pager.limit = 20;
Pager.getOld();
diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index a6fe9fb65e9..b69e5f9a3a0 100644
--- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -2,7 +2,6 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
-import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -24,7 +23,7 @@ describe('stop_jobs_modal.vue', () => {
describe('onSubmit', () => {
it('stops jobs and redirects to overview page', (done) => {
const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`;
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(props.url);
return Promise.resolve({
@@ -44,7 +43,7 @@ describe('stop_jobs_modal.vue', () => {
it('displays error if stopping jobs failed', (done) => {
const dummyError = new Error('stopping jobs failed');
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(props.url);
return Promise.reject(dummyError);
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
index 6074e06fcec..94401beb5c9 100644
--- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -3,7 +3,6 @@ import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
-import * as urlUtility from '~/lib/utils/url_utility';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -40,7 +39,7 @@ describe('delete_milestone_modal.vue', () => {
},
});
});
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.then(() => {
@@ -60,7 +59,7 @@ describe('delete_milestone_modal.vue', () => {
eventHub.$emit.calls.reset();
return Promise.reject(dummyError);
});
- const redirectSpy = spyOn(urlUtility, 'redirectTo');
+ const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.catch((error) => {
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index f95a7cef18a..fb7d2763b49 100644
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -6,7 +6,7 @@ const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
const docsUrl = 'help/ci/scheduled_pipelines';
-describe('Pipeline Schedule Callout', () => {
+describe('Pipeline Schedule Callout', function () {
beforeEach(() => {
setFixtures(`
<div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js
deleted file mode 100644
index e0ea3649646..00000000000
--- a/spec/javascripts/pipelines/async_button_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import Vue from 'vue';
-import asyncButtonComp from '~/pipelines/components/async_button.vue';
-import eventHub from '~/pipelines/event_hub';
-
-describe('Pipelines Async Button', () => {
- let component;
- let AsyncButtonComponent;
-
- beforeEach(() => {
- AsyncButtonComponent = Vue.extend(asyncButtonComp);
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'repeat',
- cssClass: 'bar',
- pipelineId: 123,
- type: 'explode',
- },
- }).$mount();
- });
-
- it('should render a button', () => {
- expect(component.$el.tagName).toEqual('BUTTON');
- });
-
- it('should render svg icon', () => {
- expect(component.$el.querySelector('svg')).not.toBeNull();
- });
-
- it('should render the provided title', () => {
- expect(component.$el.getAttribute('data-original-title')).toContain('Foo');
- expect(component.$el.getAttribute('aria-label')).toContain('Foo');
- });
-
- it('should render the provided cssClass', () => {
- expect(component.$el.getAttribute('class')).toContain('bar');
- });
-
- describe('With confirm dialog', () => {
- it('should call the service when confimation is positive', () => {
- eventHub.$on('openConfirmationModal', (data) => {
- expect(data.pipelineId).toEqual(123);
- expect(data.type).toEqual('explode');
- });
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'fa fa-foo',
- cssClass: 'bar',
- pipelineId: 123,
- type: 'explode',
- },
- }).$mount();
-
- component.$el.click();
- });
- });
-});
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index 71f77e5f42e..1e41a7bfe7c 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -29,7 +29,7 @@ describe('Pipelines Empty State', () => {
expect(
component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
- ).toContain('Continous Integration can help catch bugs by running your tests automatically,');
+ ).toContain('Continuous Integration can help catch bugs by running your tests automatically,');
expect(
component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 581209f215d..568e679abe9 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -1,13 +1,19 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import actionComponent from '~/pipelines/components/graph/action_component.vue';
-import eventHub from '~/pipelines/event_hub';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => {
let component;
+ let mock;
- beforeEach((done) => {
+ beforeEach(done => {
const ActionComponent = Vue.extend(actionComponent);
+ mock = new MockAdapter(axios);
+
+ mock.onPost('foo.json').reply(200);
+
component = mountComponent(ActionComponent, {
tooltipText: 'bar',
link: 'foo',
@@ -18,30 +24,42 @@ describe('pipeline graph action component', () => {
});
afterEach(() => {
+ mock.restore();
component.$destroy();
});
- it('should emit an event with the provided link', () => {
- eventHub.$on('graphAction', (link) => {
- expect(link).toEqual('foo');
- });
- });
-
it('should render the provided title as a bootstrap tooltip', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
});
- it('should update bootstrap tooltip when title changes', (done) => {
+ it('should update bootstrap tooltip when title changes', done => {
component.tooltipText = 'changed';
- setTimeout(() => {
+ component.$nextTick()
+ .then(() => {
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
- done();
- });
+ })
+ .then(done)
+ .catch(done.fail);
});
it('should render an svg', () => {
expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined();
expect(component.$el.querySelector('svg')).toBeDefined();
});
+
+ describe('on click', () => {
+ it('emits `pipelineActionRequestComplete` after a successfull request', done => {
+ spyOn(component, '$emit');
+
+ component.$el.click();
+
+ component.$nextTick()
+ .then(() => {
+ expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
deleted file mode 100644
index ba721bc53c6..00000000000
--- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue';
-import dropdownActionComponent from '~/pipelines/components/graph/dropdown_action_component.vue';
-
-describe('action component', () => {
- let component;
-
- beforeEach((done) => {
- const DropdownActionComponent = Vue.extend(dropdownActionComponent);
- component = new DropdownActionComponent({
- propsData: {
- tooltipText: 'bar',
- link: 'foo',
- actionMethod: 'post',
- actionIcon: 'cancel',
- },
- }).$mount();
-
- Vue.nextTick(done);
- });
-
- it('should render a link', () => {
- expect(component.$el.getAttribute('href')).toEqual('foo');
- });
-
- it('should render the provided title as a bootstrap tooltip', () => {
- expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
- });
-
- it('should render an svg', () => {
- expect(component.$el.querySelector('svg')).toBeDefined();
- });
-});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index c9677ae209a..073dae56c25 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -93,17 +93,6 @@ describe('pipeline graph job component', () => {
});
});
- describe('dropdown', () => {
- it('should render the dropdown action icon', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- isDropdown: true,
- });
-
- expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
- });
- });
-
it('should render provided class name', () => {
component = mountComponent(JobComponent, {
job: mockJob,
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
index 59092e0f041..03ead6cd8ba 100644
--- a/spec/javascripts/pipelines/mock_data.js
+++ b/spec/javascripts/pipelines/mock_data.js
@@ -217,7 +217,7 @@ export const pipelineWithStages = {
browse_path: '/gitlab-org/gitlab-ee/-/jobs/62411442/artifacts/browse',
},
{
- name: 'codequality',
+ name: 'code_quality',
expired: false,
expire_at: '2018-04-18T14:16:24.484Z',
path: '/gitlab-org/gitlab-ee/-/jobs/62411441/artifacts/download',
@@ -321,6 +321,103 @@ export const pipelineWithStages = {
};
export const stageReply = {
- html:
- '\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="karma - failed \u0026lt;br\u0026gt; (script failure)" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62402048"\u003e\u003cspan class="ci-status-icon ci-status-icon-failed"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_failed"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ekarma\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62402048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="codequality - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398081"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003ecodequality\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398081/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:check-schema-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398066"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:check-schema-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398066/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398065"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398065/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:migrate:reset-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398064"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:migrate:reset-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398064/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398070"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398070/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="db:rollback-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398069"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edb:rollback-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398069/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="dependency_scanning - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398083"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edependency_scanning\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398083/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="docs lint - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398061"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edocs lint\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398061/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="downtime_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398062"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003edowntime_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398062/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="ee_compat_check - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398063"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eee_compat_check\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398063/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:assets:compile - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398075"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:assets:compile\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398075/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398073"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398073/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab:setup-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398071"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab:setup-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398071/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="gitlab_git_test - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398086"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003egitlab_git_test\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398086/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-mysql - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398068"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-mysql\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398068/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="migration:path-pg - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398067"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003emigration:path-pg\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398067/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:internal - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398084"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:internal\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398084/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="qa:selectors - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398085"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003eqa:selectors\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398085/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398020"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398020/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398022"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398022/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398033"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398033/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398034"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398034/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398035"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398035/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398036"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398036/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398037"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398037/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398038"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398038/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398039"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398039/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398040"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398040/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398041"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398041/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398042"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398042/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398024"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398024/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398043"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398043/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398044"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398044/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398046"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398046/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398047"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398047/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398048"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398048/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398049"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398049/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398050"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398050/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398051"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398051/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398025"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398025/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398027"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398027/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398028"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398028/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398029"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398029/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398030"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398030/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398031"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398031/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-mysql 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398032"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-mysql 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398032/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 0 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397981"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 0 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397981/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 1 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397985"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 1 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397985/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 10 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398000"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 10 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398000/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 11 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398001"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 11 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398001/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 12 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398002"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 12 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398002/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 13 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398003"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 13 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398003/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 14 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398004"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 14 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398004/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 15 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398006"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 15 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398006/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 16 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398007"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 16 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398007/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 17 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398008"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 17 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398008/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 18 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398009"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 18 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398009/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 19 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398010"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 19 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398010/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 2 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397986"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 2 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397986/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 20 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398012"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 20 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398012/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 21 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398013"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 21 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398013/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 22 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398014"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 22 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398014/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 23 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398015"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 23 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398015/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 24 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398016"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 24 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398016/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 25 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398017"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 25 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398017/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 26 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398018"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 26 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398018/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 27 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398019"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 27 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398019/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 3 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397988"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 3 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397988/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 4 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397989"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 4 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397989/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 5 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397991"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 5 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397991/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 6 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397993"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 6 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397993/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 7 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397994"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 7 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397994/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 8 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397995"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 8 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397995/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="rspec-pg 9 28 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62397996"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003erspec-pg 9 28\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62397996/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="sast - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398082"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003esast\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398082/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398058"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398058/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-mysql 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398059"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-mysql 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398059/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 0 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398053"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 0 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398053/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="spinach-pg 1 2 - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398056"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003espinach-pg 1 2\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398056/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003ca class="mini-pipeline-graph-dropdown-item" data-toggle="tooltip" data-title="static-analysis - passed" data-html="true" data-container="body" href="/gitlab-org/gitlab-ce/-/jobs/62398060"\u003e\u003cspan class="ci-status-icon ci-status-icon-success"\u003e\u003csvg\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#status_success"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/span\u003e\n\u003cspan class="ci-build-text"\u003estatic-analysis\u003c/span\u003e\n\u003c/a\u003e\u003ca class="ci-action-icon-wrapper js-ci-action-icon" data-toggle="tooltip" data-title="Retry" data-container="body" rel="nofollow" data-method="post" href="/gitlab-org/gitlab-ce/-/jobs/62398060/retry"\u003e\u003csvg class=" icon-action-retry"\u003e\u003cuse xlink:href="https://gitlab.com/assets/icons-fe86f87a3d244c952cc0ec8d7f88c5effefcbe454d751d8449d4a1a32aaaf9a0.svg#retry"\u003e\u003c/use\u003e\u003c/svg\u003e\n\u003c/a\u003e\n\u003c/li\u003e\n',
+ name: 'deploy',
+ title: 'deploy: running',
+ latest_statuses: [
+ {
+ id: 928,
+ name: 'stop staging',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/928',
+ cancel_path: '/twitter/flight/-/jobs/928/cancel',
+ playable: false,
+ created_at: '2018-04-04T20:02:02.728Z',
+ updated_at: '2018-04-04T20:02:02.766Z',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/928',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico',
+ action: {
+ icon: 'cancel',
+ title: 'Cancel',
+ path: '/twitter/flight/-/jobs/928/cancel',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 926,
+ name: 'production',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/926',
+ retry_path: '/twitter/flight/-/jobs/926/retry',
+ play_path: '/twitter/flight/-/jobs/926/play',
+ playable: true,
+ created_at: '2018-04-04T20:00:57.202Z',
+ updated_at: '2018-04-04T20:11:13.110Z',
+ status: {
+ icon: 'status_canceled',
+ text: 'canceled',
+ label: 'manual play action',
+ group: 'canceled',
+ tooltip: 'canceled',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/926',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/twitter/flight/-/jobs/926/play',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 217,
+ name: 'staging',
+ started: '2018-03-07T08:41:46.234Z',
+ build_path: '/twitter/flight/-/jobs/217',
+ retry_path: '/twitter/flight/-/jobs/217/retry',
+ playable: false,
+ created_at: '2018-03-07T14:41:58.093Z',
+ updated_at: '2018-03-07T14:41:58.093Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/217',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/twitter/flight/-/jobs/217/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ tooltip: 'running',
+ has_details: true,
+ details_path: '/twitter/flight/pipelines/13#deploy',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ path: '/twitter/flight/pipelines/13#deploy',
+ dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy',
};
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index de744739e42..68043b91bd0 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import tableRowComp from '~/pipelines/components/pipelines_table_row.vue';
+import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
@@ -151,13 +152,46 @@ describe('Pipelines Table Row', () => {
describe('actions column', () => {
beforeEach(() => {
- component = buildComponent(pipeline);
+ const withActions = Object.assign({}, pipeline);
+ withActions.flags.cancelable = true;
+ withActions.flags.retryable = true;
+ withActions.cancel_path = '/cancel';
+ withActions.retry_path = '/retry';
+
+ component = buildComponent(withActions);
});
it('should render the provided actions', () => {
- expect(
- component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length,
- ).toEqual(pipeline.details.manual_actions.length);
+ expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull();
+ expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull();
+ });
+
+ it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
+ eventHub.$on('retryPipeline', (endpoint) => {
+ expect(endpoint).toEqual('/retry');
+ });
+
+ component.$el.querySelector('.js-pipelines-retry-button').click();
+ expect(component.isRetrying).toEqual(true);
+ });
+
+ it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
+ eventHub.$on('openConfirmationModal', (data) => {
+ expect(data.endpoint).toEqual('/cancel');
+ expect(data.pipelineId).toEqual(pipeline.id);
+ });
+
+ component.$el.querySelector('.js-pipelines-cancel-button').click();
+ });
+
+ it('renders a loading icon when `cancelingPipeline` matches pipeline id', done => {
+ component.cancelingPipeline = pipeline.id;
+ component.$nextTick()
+ .then(() => {
+ expect(component.isCancelling).toEqual(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index be1632e7206..16f6db39d6a 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import stage from '~/pipelines/components/stage.vue';
import eventHub from '~/pipelines/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { stageReply } from './mock_data';
describe('Pipelines stage component', () => {
let StageComponent;
@@ -41,7 +42,7 @@ describe('Pipelines stage component', () => {
describe('with successfull request', () => {
beforeEach(() => {
- mock.onGet('path.json').reply(200, { html: 'foo' });
+ mock.onGet('path.json').reply(200, stageReply);
});
it('should render the received data and emit `clickedDropdown` event', done => {
@@ -51,7 +52,7 @@ describe('Pipelines stage component', () => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
- ).toEqual('foo');
+ ).toContain(stageReply.latest_statuses[0].name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
done();
}, 0);
@@ -74,7 +75,9 @@ describe('Pipelines stage component', () => {
describe('update endpoint correctly', () => {
beforeEach(() => {
- mock.onGet('bar.json').reply(200, { html: 'this is the updated content' });
+ const copyStage = Object.assign({}, stageReply);
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
});
it('should update the stage to request the new endpoint provided', done => {
@@ -93,10 +96,37 @@ describe('Pipelines stage component', () => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
- ).toEqual('this is the updated content');
+ ).toContain('this is the updated content');
done();
});
});
});
});
+
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
+
+ describe('within pipeline table', () => {
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => {
+ spyOn(eventHub, '$emit');
+
+ component.type = 'PIPELINES_TABLE';
+ component.$el.querySelector('button').click();
+
+ setTimeout(() => {
+ component.$el.querySelector('.js-ci-action').click();
+ component.$nextTick()
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ })
+ .then(done)
+ .catch(done.fail);
+ }, 0);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js
index 2054fef790b..38b31c3d727 100644
--- a/spec/javascripts/projects_dropdown/components/app_spec.js
+++ b/spec/javascripts/projects_dropdown/components/app_spec.js
@@ -23,17 +23,18 @@ const createComponent = () => {
});
};
-const returnServicePromise = (data, failed) => new Promise((resolve, reject) => {
- if (failed) {
- reject(data);
- } else {
- resolve({
- json() {
- return data;
- },
- });
- }
-});
+const returnServicePromise = (data, failed) =>
+ new Promise((resolve, reject) => {
+ if (failed) {
+ reject(data);
+ } else {
+ resolve({
+ json() {
+ return data;
+ },
+ });
+ }
+ });
describe('AppComponent', () => {
describe('computed', () => {
@@ -185,7 +186,7 @@ describe('AppComponent', () => {
describe('fetchSearchedProjects', () => {
const searchQuery = 'test';
- it('should perform search with provided search query', (done) => {
+ it('should perform search with provided search query', done => {
const mockData = [mockRawProject];
spyOn(vm, 'toggleLoader');
spyOn(vm, 'toggleSearchProjectsList');
@@ -203,7 +204,7 @@ describe('AppComponent', () => {
}, 0);
});
- it('should update props for showing search failure', (done) => {
+ it('should update props for showing search failure', done => {
spyOn(vm, 'toggleSearchProjectsList');
spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true));
@@ -219,7 +220,7 @@ describe('AppComponent', () => {
});
describe('logCurrentProjectAccess', () => {
- it('should log current project access via service', (done) => {
+ it('should log current project access via service', done => {
spyOn(vm.service, 'logProjectAccess');
vm.currentProject = mockProject;
@@ -257,7 +258,7 @@ describe('AppComponent', () => {
});
describe('created', () => {
- it('should bind event listeners on eventHub', (done) => {
+ it('should bind event listeners on eventHub', done => {
spyOn(eventHub, '$on');
createComponent().$mount();
@@ -273,7 +274,7 @@ describe('AppComponent', () => {
});
describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', (done) => {
+ it('should unbind event listeners on eventHub', done => {
const vm = createComponent();
spyOn(eventHub, '$off');
@@ -305,7 +306,7 @@ describe('AppComponent', () => {
expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
});
- it('should render loading animation', (done) => {
+ it('should render loading animation', done => {
vm.toggleLoader(true);
Vue.nextTick(() => {
const loadingEl = vm.$el.querySelector('.loading-animation');
@@ -317,7 +318,7 @@ describe('AppComponent', () => {
});
});
- it('should render frequent projects list header', (done) => {
+ it('should render frequent projects list header', done => {
vm.toggleFrequentProjectsList(true);
Vue.nextTick(() => {
const sectionHeaderEl = vm.$el.querySelector('.section-header');
@@ -328,7 +329,7 @@ describe('AppComponent', () => {
});
});
- it('should render frequent projects list', (done) => {
+ it('should render frequent projects list', done => {
vm.toggleFrequentProjectsList(true);
Vue.nextTick(() => {
expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined();
@@ -336,7 +337,7 @@ describe('AppComponent', () => {
});
});
- it('should render searched projects list', (done) => {
+ it('should render searched projects list', done => {
vm.toggleSearchProjectsList(true);
Vue.nextTick(() => {
expect(vm.$el.querySelector('.section-header')).toBe(null);
diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js
index 601264258c2..427f5024e3a 100644
--- a/spec/javascripts/projects_dropdown/components/search_spec.js
+++ b/spec/javascripts/projects_dropdown/components/search_spec.js
@@ -92,7 +92,6 @@ describe('SearchComponent', () => {
const inputEl = vm.$el.querySelector('input.form-control');
expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
- expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy();
expect(inputEl).not.toBe(null);
expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
expect(vm.$el.querySelector('.search-icon')).toBeDefined();
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 80770a61011..e264b16335f 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -9,8 +9,6 @@ import Sidebar from '~/right_sidebar';
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
- this.sidebar = null;
-
$aside = null;
$toggle = null;
@@ -43,7 +41,7 @@ import Sidebar from '~/right_sidebar';
beforeEach(function() {
loadFixtures(fixtureName);
mock = new MockAdapter(axios);
- this.sidebar = new Sidebar();
+ new Sidebar(); // eslint-disable-line no-new
$aside = $('.right-sidebar');
$page = $('.layout-page');
$icon = $aside.find('i');
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 1a27955983d..4f515f98a7e 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -4,7 +4,6 @@ import $ from 'jquery';
import '~/gl_dropdown';
import SearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('Search autocomplete dropdown', () => {
var assertLinks,
@@ -129,9 +128,6 @@ describe('Search autocomplete dropdown', () => {
beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw');
- // Prevent turbolinks from triggering within gl_dropdown
- spyOn(urlUtils, 'visitUrl').and.returnValue(true);
-
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js
index d433f8c3e07..4fba36bd4de 100644
--- a/spec/javascripts/settings_panels_spec.js
+++ b/spec/javascripts/settings_panels_spec.js
@@ -13,9 +13,9 @@ describe('Settings Panels', () => {
});
it('should expand linked hash fragment panel', () => {
- location.hash = '#js-general-pipeline-settings';
+ location.hash = '#autodevops-settings';
- const pipelineSettingsPanel = document.querySelector('#js-general-pipeline-settings');
+ const pipelineSettingsPanel = document.querySelector('#autodevops-settings');
// Our test environment automatically expands everything so we need to clear that out first
pipelineSettingsPanel.classList.remove('expanded');
diff --git a/spec/javascripts/shortcuts_dashboard_navigation_spec.js b/spec/javascripts/shortcuts_dashboard_navigation_spec.js
index 888b49004bf..7cb201e01d8 100644
--- a/spec/javascripts/shortcuts_dashboard_navigation_spec.js
+++ b/spec/javascripts/shortcuts_dashboard_navigation_spec.js
@@ -1,24 +1,23 @@
import findAndFollowLink from '~/shortcuts_dashboard_navigation';
-import * as urlUtility from '~/lib/utils/url_utility';
describe('findAndFollowLink', () => {
it('visits a link when the selector exists', () => {
const href = '/some/path';
- const locationSpy = spyOn(urlUtility, 'visitUrl');
+ const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
findAndFollowLink('.my-shortcut');
- expect(locationSpy).toHaveBeenCalledWith(href);
+ expect(visitUrl).toHaveBeenCalledWith(href);
});
it('does not throw an exception when the selector does not exist', () => {
- const locationSpy = spyOn(urlUtility, 'visitUrl');
+ const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
// this should not throw an exception
findAndFollowLink('.this-selector-does-not-exist');
- expect(locationSpy).not.toHaveBeenCalled();
+ expect(visitUrl).not.toHaveBeenCalled();
});
});
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index b0d714cbefb..d73608ed0ed 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -4,7 +4,7 @@ import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
-describe('ShortcutsIssuable', () => {
+describe('ShortcutsIssuable', function () {
const fixtureName = 'merge_requests/diff_comment.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 8b6e8b24f00..fcd7bea3f6d 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -138,7 +138,7 @@ const RESPONSE_MAP = {
},
{
id: 20,
- name_with_namespace: 'foo / bar',
+ name_with_namespace: '<img src=x onerror=alert(document.domain)> foo / bar',
},
],
},
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
index 2a3b60c399c..e796ddee62f 100644
--- a/spec/javascripts/sidebar/participants_spec.js
+++ b/spec/javascripts/sidebar/participants_spec.js
@@ -170,5 +170,19 @@ describe('Participants', function () {
expect(vm.isShowingMoreParticipants).toBe(true);
});
+
+ it('clicking on participants icon emits `toggleSidebar` event', () => {
+ vm = mountComponent(Participants, {
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+ spyOn(vm, '$emit');
+
+ const participantsIconEl = vm.$el.querySelector('.sidebar-collapsed-icon');
+
+ participantsIconEl.click();
+ expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
+ });
});
});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index afa18cc127e..da950258a94 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -1,12 +1,11 @@
import _ from 'underscore';
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
import Mock from './mock_data';
-describe('Sidebar mediator', () => {
+describe('Sidebar mediator', function() {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.mediator = new SidebarMediator(Mock.mediator);
@@ -87,12 +86,12 @@ describe('Sidebar mediator', () => {
const moveToProjectId = 7;
this.mediator.store.setMoveToProjectId(moveToProjectId);
spyOn(this.mediator.service, 'moveIssue').and.callThrough();
- spyOn(urlUtils, 'visitUrl');
+ const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl');
this.mediator.moveIssue()
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+ expect(visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
index d8e636cbdf0..8f35b9ca437 100644
--- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -7,14 +7,16 @@ import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
import Mock from './mock_data';
-describe('SidebarMoveIssue', () => {
+describe('SidebarMoveIssue', function () {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
this.mediator = new SidebarMediator(Mock.mediator);
this.$content = $(`
<div class="dropdown">
<div class="js-toggle"></div>
- <div class="dropdown-content"></div>
+ <div class="dropdown-menu">
+ <div class="dropdown-content"></div>
+ </div>
<div class="js-confirm-button"></div>
</div>
`);
@@ -69,6 +71,15 @@ describe('SidebarMoveIssue', () => {
expect($.fn.glDropdown).toHaveBeenCalled();
});
+
+ it('escapes html from project name', (done) => {
+ this.$toggleButton.dropdown('toggle');
+
+ setTimeout(() => {
+ expect(this.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual('&lt;img src=x onerror=alert(document.domain)&gt; foo / bar');
+ done();
+ });
+ });
});
describe('onConfirmClicked', () => {
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index 3591f96ff87..08b112a54ba 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -31,7 +31,7 @@ const PARTICIPANT_LIST = [
{ ...PARTICIPANT, id: 3 },
];
-describe('Sidebar store', () => {
+describe('Sidebar store', function () {
beforeEach(() => {
this.store = new SidebarStore({
currentUser: {
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
index 56a2543660b..9e437084224 100644
--- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -3,7 +3,6 @@ import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_sub
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
-import eventHub from '~/sidebar/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Mock from './mock_data';
@@ -32,7 +31,7 @@ describe('Sidebar Subscriptions', function () {
mediator,
});
- eventHub.$emit('toggleSubscription');
+ vm.onToggleSubscription();
expect(mediator.toggleSubscription).toHaveBeenCalled();
});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
index aee8f0acbb9..f0a53e573c3 100644
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import eventHub from '~/sidebar/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Subscriptions', function () {
@@ -39,4 +40,22 @@ describe('Subscriptions', function () {
expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass('is-checked');
});
+
+ it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
+ vm = mountComponent(Subscriptions, { subscribed: true });
+ spyOn(eventHub, '$emit');
+ spyOn(vm, '$emit');
+
+ vm.toggleSubscription();
+ expect(eventHub.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
+ expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
+ });
+
+ it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
+ vm = mountComponent(Subscriptions, { subscribed: true });
+ spyOn(vm, '$emit');
+
+ vm.onClickCollapsedIcon();
+ expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
+ });
});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 14bff05e537..2411d33a496 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -1,4 +1,5 @@
-/* eslint-disable jasmine/no-global-setup */
+/* eslint-disable jasmine/no-global-setup, jasmine/no-unsafe-spy, no-underscore-dangle */
+
import $ from 'jquery';
import 'vendor/jasmine-jquery';
import '~/commons';
@@ -55,6 +56,17 @@ window.addEventListener('unhandledrejection', event => {
console.error(event.reason.stack || event.reason);
});
+// Add global function to spy on a module's dependencies via rewire
+window.spyOnDependency = (module, name) => {
+ const dependency = module.__GetDependency__(name);
+ const spy = jasmine.createSpy(name, dependency);
+ module.__Rewire__(name, spy);
+ return spy;
+};
+
+// Reset any rewired modules after each test (see babel-plugin-rewire)
+afterEach(__rewire_reset_all__); // eslint-disable-line
+
// HACK: Chrome 59 disconnects if there are too many synchronous tests in a row
// because it appears to lock up the thread that communicates to Karma's socket
// This async beforeEach gets called on every spec and releases the JS thread long
@@ -72,21 +84,11 @@ beforeEach(() => {
const axiosDefaultAdapter = getDefaultAdapter();
-let testFiles = process.env.TEST_FILES || [];
-if (testFiles.length > 0) {
- testFiles = testFiles.map(path => path.replace(/^spec\/javascripts\//, '').replace(/\.js$/, ''));
- console.log(`Running only tests matching: ${testFiles}`);
-} else {
- console.log('Running all tests');
-}
-
// render all of our tests
const testsContext = require.context('.', true, /_spec$/);
testsContext.keys().forEach(function(path) {
try {
- if (testFiles.length === 0 || testFiles.some(p => path.includes(p))) {
- testsContext(path);
- }
+ testsContext(path);
} catch (err) {
console.error('[ERROR] Unable to load spec: ', path);
describe('Test bundle', function() {
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index 898bbb3819b..e74f4bdef7e 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import * as urlUtils from '~/lib/utils/url_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils';
@@ -18,7 +17,7 @@ describe('Todos', () => {
it('opens the todo url', (done) => {
const todoLink = todoItem.dataset.url;
- spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
+ spyOnDependency(Todos, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(todoLink);
done();
});
@@ -33,7 +32,7 @@ describe('Todos', () => {
beforeEach(() => {
metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
- visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {});
+ visitUrlSpy = spyOnDependency(Todos, 'visitUrl').and.callFake(() => {});
windowOpenSpy = spyOn(window, 'open').and.callFake(() => {});
});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 39c47a5c06d..d84b13b07c4 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -3,7 +3,7 @@ import U2FAuthenticate from '~/u2f/authenticate';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
-describe('U2FAuthenticate', () => {
+describe('U2FAuthenticate', function () {
preloadFixtures('u2f/authenticate.html.raw');
beforeEach((done) => {
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 136b4cad737..d9383314891 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -3,7 +3,7 @@ import U2FRegister from '~/u2f/register';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
-describe('U2FRegister', () => {
+describe('U2FRegister', function () {
preloadFixtures('u2f/register.html.raw');
beforeEach((done) => {
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index ff8d54c029f..c82ba61a5b1 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
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';
@@ -117,13 +116,13 @@ describe('Deployment component', () => {
it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
- spyOn(urlUtils, 'visitUrl').and.returnValue(true);
+ const visitUrl = spyOnDependency(deploymentComponent, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith(url);
+ expect(visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
index db27aa144d6..00f4f2d7c39 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
-import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue';
+import MrWidgetAuthor from '~/vue_merge_request_widget/components/mr_widget_author.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-describe('MRWidgetAuthor', () => {
+describe('MrWidgetAuthor', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(authorComponent);
+ const Component = Vue.extend(MrWidgetAuthor);
vm = mountComponent(Component, {
author: {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js
deleted file mode 100644
index cee22d5342a..00000000000
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import Vue from 'vue';
-import maintainerEditComponent from '~/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('RWidgetMaintainerEdit', () => {
- let Component;
- let vm;
-
- beforeEach(() => {
- Component = Vue.extend(maintainerEditComponent);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('when a maintainer is allowed to edit', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- maintainerEditAllowed: true,
- });
- });
-
- it('it renders the message', () => {
- expect(vm.$el.textContent.trim()).toEqual('Allows edits from maintainers');
- });
- });
-
- describe('when a maintainer is not allowed to edit', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- maintainerEditAllowed: false,
- });
- });
-
- it('hides the message', () => {
- expect(vm.$el.textContent.trim()).toEqual('');
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index dd1d62cd4ed..a0a74648328 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -4,21 +4,37 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetFailedToMerge', () => {
+ const dummyIntervalId = 1337;
let Component;
let vm;
beforeEach(() => {
Component = Vue.extend(failedToMergeComponent);
spyOn(eventHub, '$emit');
- vm = mountComponent(Component, { mr: {
- mergeError: 'Merge error happened.',
- } });
+ spyOn(window, 'setInterval').and.returnValue(dummyIntervalId);
+ spyOn(window, 'clearInterval').and.stub();
+ vm = mountComponent(Component, {
+ mr: {
+ mergeError: 'Merge error happened.',
+ },
+ });
});
afterEach(() => {
vm.$destroy();
});
+ it('sets interval to refresh', () => {
+ expect(window.setInterval).toHaveBeenCalledWith(vm.updateTimer, 1000);
+ expect(vm.intervalId).toBe(dummyIntervalId);
+ });
+
+ it('clears interval when destroying ', () => {
+ vm.$destroy();
+
+ expect(window.clearInterval).toHaveBeenCalledWith(dummyIntervalId);
+ });
+
describe('computed', () => {
describe('timerText', () => {
it('should return correct timer text', () => {
@@ -65,11 +81,13 @@ describe('MRWidgetFailedToMerge', () => {
});
describe('while it is refreshing', () => {
- it('renders Refresing now', (done) => {
+ it('renders Refresing now', done => {
vm.isRefreshing = true;
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual('Refreshing now');
+ expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual(
+ 'Refreshing now',
+ );
done();
});
});
@@ -78,11 +96,15 @@ describe('MRWidgetFailedToMerge', () => {
describe('while it is not regresing', () => {
it('renders warning icon and disabled merge button', () => {
expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
- expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual('disabled');
+ expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual(
+ 'disabled',
+ );
});
it('renders given error', () => {
- expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual('Merge error happened..');
+ expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
+ 'Merge error happened..',
+ );
});
it('renders refresh button', () => {
@@ -90,13 +112,13 @@ describe('MRWidgetFailedToMerge', () => {
});
it('renders remaining time', () => {
- expect(
- vm.$el.querySelector('.has-custom-error').textContent.trim(),
- ).toEqual('Refreshing in 10 seconds to show the updated status...');
+ expect(vm.$el.querySelector('.has-custom-error').textContent.trim()).toEqual(
+ 'Refreshing in 10 seconds to show the updated status...',
+ );
});
});
- it('should just generic merge failed message if merge_error is not available', (done) => {
+ it('should just generic merge failed message if merge_error is not available', done => {
vm.mr.mergeError = null;
Vue.nextTick(() => {
@@ -106,7 +128,7 @@ describe('MRWidgetFailedToMerge', () => {
});
});
- it('should show refresh label when refresh requested', (done) => {
+ it('should show refresh label when refresh requested', done => {
vm.refresh();
Vue.nextTick(() => {
expect(vm.$el.innerText).not.toContain('Merge failed. Refreshing');
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 c2c92d8ac56..adeea03481f 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
@@ -6,6 +6,14 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMerged', () => {
let vm;
const targetBranch = 'foo';
+ const selectors = {
+ get copyMergeShaButton() {
+ return vm.$el.querySelector('button.js-mr-merged-copy-sha');
+ },
+ get mergeCommitShaLink() {
+ return vm.$el.querySelector('a.js-mr-merged-commit-sha');
+ },
+ };
beforeEach(() => {
const Component = Vue.extend(mergedComponent);
@@ -31,6 +39,9 @@ describe('MRWidgetMerged', () => {
readableClosedAt: '',
},
updatedAt: 'mergedUpdatedAt',
+ shortMergeCommitSha: 'asdf1234',
+ mergeCommitPath: 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d',
+ sourceBranch: 'bar',
targetBranch,
};
@@ -140,6 +151,17 @@ describe('MRWidgetMerged', () => {
expect(vm.$el.textContent).toContain('Cherry-pick');
});
+ it('shows button to copy commit SHA to clipboard', () => {
+ expect(selectors.copyMergeShaButton).toExist();
+ expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(vm.mr.shortMergeCommitSha);
+ });
+
+ it('shows merge commit SHA link', () => {
+ expect(selectors.mergeCommitShaLink).toExist();
+ expect(selectors.mergeCommitShaLink.text).toContain(vm.mr.shortMergeCommitSha);
+ expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath);
+ });
+
it('should not show source branch removed text', (done) => {
vm.mr.sourceBranchRemoved = false;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 300b7882d03..81c16593eb4 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import * as simplePoll from '~/lib/utils/simple_poll';
const commitMessage = 'This is the commit message';
const commitMessageWithDescription = 'This is the commit message description';
@@ -355,9 +354,9 @@ describe('ReadyToMerge', () => {
describe('initiateMergePolling', () => {
it('should call simplePoll', () => {
- spyOn(simplePoll, 'default');
+ const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateMergePolling();
- expect(simplePoll.default).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalled();
});
});
@@ -457,11 +456,11 @@ describe('ReadyToMerge', () => {
describe('initiateRemoveSourceBranchPolling', () => {
it('should emit event and call simplePoll', () => {
spyOn(eventHub, '$emit');
- spyOn(simplePoll, 'default');
+ const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateRemoveSourceBranchPolling();
expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
- expect(simplePoll.default).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalled();
});
});
@@ -524,18 +523,20 @@ describe('ReadyToMerge', () => {
});
describe('when user can merge and can delete branch', () => {
+ let customVm;
+
beforeEach(() => {
- this.customVm = createComponent({
+ customVm = createComponent({
mr: { canRemoveSourceBranch: true },
});
});
it('isRemoveSourceBranchButtonDisabled should be false', () => {
- expect(this.customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
+ expect(customVm.isRemoveSourceBranchButtonDisabled).toBe(false);
});
it('should be enabled in rendered output', () => {
- const checkboxElement = this.customVm.$el.querySelector('#remove-source-branch-input');
+ const checkboxElement = customVm.$el.querySelector('#remove-source-branch-input');
expect(checkboxElement).not.toBeNull();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 98ab61a0367..cea603368bf 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import wipComponent from '~/vue_merge_request_widget/components/states/mr_widget_wip';
+import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
const createComponent = () => {
- const Component = Vue.extend(wipComponent);
+ const Component = Vue.extend(WorkInProgress);
const mr = {
title: 'The best MR ever',
removeWIPPath: '/path/to/remove/wip',
@@ -17,10 +17,10 @@ const createComponent = () => {
});
};
-describe('MRWidgetWIP', () => {
+describe('Wip', () => {
describe('props', () => {
it('should have props', () => {
- const { mr, service } = wipComponent.props;
+ const { mr, service } = WorkInProgress.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 3fc7663b9c2..9d2a15ff009 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -18,6 +18,7 @@ export default {
human_total_time_spent: null,
in_progress_merge_commit_sha: null,
merge_commit_sha: '53027d060246c8f47e4a9310fb332aa52f221775',
+ short_merge_commit_sha: '53027d06',
merge_error: null,
merge_params: {
force_remove_source_branch: null,
@@ -215,4 +216,5 @@ 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',
};
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 e55c7649d40..30918428da2 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
+import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
diff --git a/spec/javascripts/vue_shared/components/callout_spec.js b/spec/javascripts/vue_shared/components/callout_spec.js
new file mode 100644
index 00000000000..e62bd86f4ca
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/callout_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import callout from '~/vue_shared/components/callout.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('Callout Component', () => {
+ let CalloutComponent;
+ let vm;
+ const exampleMessage = 'This is a callout message!';
+
+ beforeEach(() => {
+ CalloutComponent = Vue.extend(callout);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render the appropriate variant of callout', () => {
+ vm = createComponent(CalloutComponent, {
+ category: 'info',
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.getAttribute('class')).toEqual('bs-callout bs-callout-info');
+
+ expect(vm.$el.tagName).toEqual('DIV');
+ });
+
+ it('should render accessibility attributes', () => {
+ vm = createComponent(CalloutComponent, {
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.getAttribute('role')).toEqual('alert');
+ expect(vm.$el.getAttribute('aria-live')).toEqual('assertive');
+ });
+
+ it('should render the provided message', () => {
+ vm = createComponent(CalloutComponent, {
+ message: exampleMessage,
+ });
+
+ expect(vm.$el.innerHTML.trim()).toEqual(exampleMessage);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js
index d8664408595..423bc746a22 100644
--- a/spec/javascripts/vue_shared/components/ci_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_icon_spec.js
@@ -1,139 +1,122 @@
import Vue from 'vue';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('CI Icon component', () => {
- let CiIcon;
- beforeEach(() => {
- CiIcon = Vue.extend(ciIcon);
+ const Component = Vue.extend(ciIcon);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
});
it('should render a span element with an svg', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_success',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_success',
},
- }).$mount();
+ });
- expect(component.$el.tagName).toEqual('SPAN');
- expect(component.$el.querySelector('span > svg')).toBeDefined();
+ expect(vm.$el.tagName).toEqual('SPAN');
+ expect(vm.$el.querySelector('span > svg')).toBeDefined();
});
it('should render a success status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_success',
- group: 'success',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_success',
+ group: 'success',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-success')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true);
});
it('should render a failed status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_failed',
- group: 'failed',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_failed',
+ group: 'failed',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
});
it('should render success with warnings status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_warning',
- group: 'warning',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_warning',
+ group: 'warning',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
});
it('should render pending status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_pending',
- group: 'pending',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_pending',
+ group: 'pending',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
});
it('should render running status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_running',
- group: 'running',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_running',
+ group: 'running',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-running')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true);
});
it('should render created status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_created',
- group: 'created',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_created',
+ group: 'created',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-created')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true);
});
it('should render skipped status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_skipped',
- group: 'skipped',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_skipped',
+ group: 'skipped',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
});
it('should render canceled status', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_canceled',
- group: 'canceled',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_canceled',
+ group: 'canceled',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
});
it('should render status for manual action', () => {
- const component = new CiIcon({
- propsData: {
- status: {
- icon: 'icon_status_manual',
- group: 'manual',
- },
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'icon_status_manual',
+ group: 'manual',
},
- }).$mount();
+ });
- expect(component.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
+ expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
});
});
diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
index f598b1afa74..97f0fbb04db 100644
--- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js
+++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
@@ -3,10 +3,10 @@ import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('clipboard button', () => {
+ const Component = Vue.extend(clipboardButton);
let vm;
beforeEach(() => {
- const Component = Vue.extend(clipboardButton);
vm = mountComponent(Component, {
text: 'copy me',
title: 'Copy this value into Clipboard!',
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index ed66361bfc3..7189e8cfcfa 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -55,7 +55,6 @@ describe('Commit component', () => {
path: '/jschatz1',
username: 'jschatz1',
},
- commitIconSvg: '<svg></svg>',
};
component = mountComponent(CommitComponent, props);
@@ -82,8 +81,10 @@ describe('Commit component', () => {
expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
});
- it('should render the given commitIconSvg', () => {
- expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg');
+ it('should render icon for commit', () => {
+ expect(
+ component.$el.querySelector('.js-commit-icon use').getAttribute('xlink:href'),
+ ).toContain('commit');
});
describe('Given commit title and author props', () => {
diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js
index f19589d3b75..af9693c48fd 100644
--- a/spec/javascripts/vue_shared/components/expand_button_spec.js
+++ b/spec/javascripts/vue_shared/components/expand_button_spec.js
@@ -3,10 +3,10 @@ import expandButton from '~/vue_shared/components/expand_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('expand button', () => {
+ const Component = Vue.extend(expandButton);
let vm;
beforeEach(() => {
- const Component = Vue.extend(expandButton);
vm = mountComponent(Component, {
slots: {
expanded: '<p>Expanded!</p>',
@@ -22,7 +22,7 @@ describe('expand button', () => {
expect(vm.$el.textContent.trim()).toEqual('...');
});
- it('hides expander on click', (done) => {
+ it('hides expander on click', done => {
vm.$el.querySelector('button').click();
vm.$nextTick(() => {
expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;');
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
index 6fe95153204..e8685ab48be 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -73,6 +73,22 @@ describe('BaseComponent', () => {
expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
});
});
+
+ describe('handleCollapsedValueClick', () => {
+ it('emits toggleCollapse event on component', () => {
+ spyOn(vm, '$emit');
+ vm.handleCollapsedValueClick();
+ expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
+ });
+ });
+
+ describe('handleDropdownHidden', () => {
+ it('emits onDropdownClose event on component', () => {
+ spyOn(vm, '$emit');
+ vm.handleDropdownHidden();
+ expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
+ });
+ });
});
describe('mounted', () => {
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 39040670a87..da74595bcdc 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
@@ -56,6 +56,16 @@ describe('DropdownValueCollapsedComponent', () => {
});
});
+ describe('methods', () => {
+ describe('handleClick', () => {
+ it('emits onValueClick event on component', () => {
+ spyOn(vm, '$emit');
+ vm.handleClick();
+ expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
+ });
+ });
+ });
+
describe('template', () => {
it('renders component container element with tooltip`', () => {
expect(vm.$el.dataset.placement).toBe('left');
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index a547988d631..c73c6023b60 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -7,7 +7,203 @@ describe API::Helpers::Pagination do
Class.new.include(described_class).new
end
- describe '#paginate' do
+ describe '#paginate (keyset pagination)' do
+ let(:value) { spy('return value') }
+
+ before do
+ allow(value).to receive(:to_query).and_return(value)
+
+ allow(subject).to receive(:header).and_return(value)
+ allow(subject).to receive(:params).and_return(value)
+ allow(subject).to receive(:request).and_return(value)
+ end
+
+ context 'when resource can be paginated' do
+ let!(:projects) do
+ [
+ create(:project, name: 'One'),
+ create(:project, name: 'Two'),
+ create(:project, name: 'Three')
+ ].sort_by { |e| -e.id } # sort by id desc (this is the default sort order for the API)
+ end
+
+ describe 'first page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 2
+ end
+
+ it 'returns the first two records (by id desc)' do
+ expect(subject.paginate(resource)).to eq(projects[0..1])
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[1].id}&pagination=keyset&per_page=2")
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+
+ describe 'second page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[1].id })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 1
+ end
+
+ it 'returns the third record' do
+ expect(subject.paginate(resource)).to eq(projects[2..2])
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[2].id}&pagination=keyset&per_page=2")
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+
+ describe 'third page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[2].id })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 0
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Per-Page', '2')
+ expect(subject).not_to receive(:header).with('Link')
+
+ subject.paginate(resource)
+ end
+ end
+
+ context 'if order' do
+ context 'is not present' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ end
+
+ it 'is not present it adds default order(:id) desc' do
+ resource.order_values = []
+
+ paginated_relation = subject.paginate(resource)
+
+ expect(resource.order_values).to be_empty
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.size).to eq(1)
+ expect(paginated_relation.order_values.first).to be_descending
+ expect(paginated_relation.order_values.first.expr.name).to eq :id
+ end
+ end
+
+ context 'is present' do
+ let(:resource) { Project.all.order(name: :desc) }
+ let!(:projects) do
+ [
+ create(:project, name: 'One'),
+ create(:project, name: 'Two'),
+ create(:project, name: 'Three'),
+ create(:project, name: 'Three'), # Note the duplicate name
+ create(:project, name: 'Four'),
+ create(:project, name: 'Five'),
+ create(:project, name: 'Six')
+ ]
+
+ # if we sort this by name descending, id descending, this yields:
+ # {
+ # 2 => "Two",
+ # 4 => "Three",
+ # 3 => "Three",
+ # 7 => "Six",
+ # 1 => "One",
+ # 5 => "Four",
+ # 6 => "Five"
+ # }
+ #
+ # (key is the id)
+ end
+
+ it 'it also orders by primary key' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ paginated_relation = subject.paginate(resource)
+
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.size).to eq(2)
+ expect(paginated_relation.order_values.first).to be_descending
+ expect(paginated_relation.order_values.first.expr.name).to eq :name
+ expect(paginated_relation.order_values.second).to be_descending
+ expect(paginated_relation.order_values.second.expr.name).to eq :id
+ end
+
+ it 'it returns the right records (first page)' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', per_page: 2 })
+ result = subject.paginate(resource)
+
+ expect(result.first).to eq(projects[1])
+ expect(result.second).to eq(projects[3])
+ end
+
+ it 'it returns the right records (second page)' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
+ result = subject.paginate(resource)
+
+ expect(result.first).to eq(projects[2])
+ expect(result.second).to eq(projects[6])
+ end
+
+ it 'it returns the right records (third page), note increased per_page' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5 })
+ result = subject.paginate(resource)
+
+ expect(result.size).to eq(3)
+ expect(result.first).to eq(projects[0])
+ expect(result.second).to eq(projects[4])
+ expect(result.last).to eq(projects[5])
+ end
+
+ it 'it returns the right link to the next page' do
+ allow(subject).to receive(:params)
+ .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2")
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#paginate (default offset-based pagination)' do
let(:value) { spy('return value') }
before do
@@ -146,14 +342,14 @@ describe API::Helpers::Pagination do
end
end
end
+ end
- def expect_header(*args, &block)
- expect(subject).to receive(:header).with(*args, &block)
- end
+ def expect_header(*args, &block)
+ expect(subject).to receive(:header).with(*args, &block)
+ end
- def expect_message(method)
- expect(subject).to receive(method)
- .at_least(:once).and_return(value)
- end
+ def expect_message(method)
+ expect(subject).to receive(method)
+ .at_least(:once).and_return(value)
end
end
diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb
index b918301f1cb..66af7f81535 100644
--- a/spec/lib/api/helpers/related_resources_helpers_spec.rb
+++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb
@@ -9,9 +9,9 @@ describe API::Helpers::RelatedResourcesHelpers do
let(:path) { '/api/v4/awesome_endpoint' }
subject(:url) { helpers.expose_url(path) }
- def stub_default_url_options(protocol: 'http', host: 'example.com', port: nil)
+ def stub_default_url_options(protocol: 'http', host: 'example.com', port: nil, script_name: '')
expect(Gitlab::Application.routes).to receive(:default_url_options)
- .and_return(protocol: protocol, host: host, port: port)
+ .and_return(protocol: protocol, host: host, port: port, script_name: script_name)
end
it 'respects the protocol if it is HTTP' do
@@ -37,5 +37,17 @@ describe API::Helpers::RelatedResourcesHelpers do
is_expected.to start_with('http://example.com:8080/')
end
+
+ it 'includes the relative_url before the path if it is set' do
+ stub_default_url_options(script_name: '/gitlab')
+
+ is_expected.to start_with('http://example.com/gitlab/api/v4')
+ end
+
+ it 'includes the path after the host' do
+ stub_default_url_options
+
+ is_expected.to start_with('http://example.com/api/v4')
+ end
end
end
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
index 14d055cbcc1..99872211a4e 100644
--- a/spec/lib/backup/files_spec.rb
+++ b/spec/lib/backup/files_spec.rb
@@ -62,5 +62,19 @@ describe Backup::Files do
subject.restore
end
end
+
+ describe 'folders that are a mountpoint' do
+ before do
+ allow(FileUtils).to receive(:mv).and_raise(Errno::EBUSY)
+ allow(subject).to receive(:run_pipeline!).and_return(true)
+ end
+
+ it 'shows error message' do
+ expect(subject).to receive(:resource_busy_error).with("/var/gitlab-registry")
+ .and_call_original
+
+ expect { subject.restore }.to raise_error(/is a mountpoint/)
+ end
+ end
end
end
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 84688845fa5..23c04a1a101 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -5,6 +5,8 @@ describe Backup::Manager do
let(:progress) { StringIO.new }
+ subject { described_class.new(progress) }
+
before do
allow(progress).to receive(:puts)
allow(progress).to receive(:print)
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index e4c1c9bafc0..023bedaaebb 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe Backup::Repository do
let(:progress) { StringIO.new }
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :wiki_repo) }
+ subject { described_class.new(progress) }
before do
allow(progress).to receive(:puts)
@@ -24,14 +25,12 @@ describe Backup::Repository do
end
it 'does not raise error' do
- expect { described_class.new.dump }.not_to raise_error
+ expect { subject.dump }.not_to raise_error
end
end
end
describe '#restore' do
- subject { described_class.new }
-
let(:timestamp) { Time.utc(2017, 3, 22) }
let(:temp_dirs) do
Gitlab.config.repositories.storages.map do |name, storage|
@@ -49,14 +48,14 @@ describe Backup::Repository do
describe 'command failure' do
before do
- allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
+ allow_any_instance_of(Gitlab::Shell).to receive(:create_repository).and_return(false)
end
context 'hashed storage' do
it 'shows the appropriate error' do
subject.restore
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} (#{project.disk_path}) - error")
+ expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository")
end
end
@@ -66,21 +65,44 @@ describe Backup::Repository do
it 'shows the appropriate error' do
subject.restore
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
+ expect(progress).to have_received(:puts).with("[Failed] restoring #{project.full_path} repository")
end
end
end
+ end
- describe 'folders without permissions' do
+ describe '#delete_all_repositories', :seed_helper do
+ shared_examples('delete_all_repositories') do
before do
- allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES)
+ allow(FileUtils).to receive(:mkdir_p).and_call_original
+ allow(FileUtils).to receive(:mv).and_call_original
+ end
+
+ after(:all) do
+ ensure_seeds
+ end
+
+ it 'removes all repositories' do
+ # Sanity check: there should be something for us to delete
+ expect(list_repositories).to include(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH))
+
+ subject.delete_all_repositories('default', Gitlab.config.repositories.storages['default'])
+
+ expect(list_repositories).to be_empty
end
- it 'shows error message' do
- expect(subject).to receive(:access_denied_error)
- subject.restore
+ def list_repositories
+ Dir[SEED_STORAGE_PATH + '/*.git']
end
end
+
+ context 'with gitaly' do
+ it_behaves_like 'delete_all_repositories'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like 'delete_all_repositories'
+ end
end
describe '#empty_repo?' do
@@ -90,20 +112,20 @@ describe Backup::Repository do
it 'invalidates the emptiness cache' do
expect(wiki.repository).to receive(:expire_emptiness_caches).once
- wiki.empty?
+ subject.send(:empty_repo?, wiki)
end
context 'wiki repo has content' do
let!(:wiki_page) { create(:wiki_page, wiki: wiki) }
it 'returns true, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be(false)
+ expect(subject.send(:empty_repo?, wiki)).to be(false)
end
end
context 'wiki repo does not have content' do
it 'returns true, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
+ expect(subject.send(:empty_repo?, wiki)).to be_truthy
end
end
end
diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
index 1fd145116df..068cdc85a07 100644
--- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
@@ -47,16 +47,36 @@ describe Banzai::Filter::CommitTrailersFilter do
)
end
- it 'non GitLab users and replaces them with mailto links' do
- _, message_html = build_commit_message(
- trailer: trailer,
- name: FFaker::Name.name,
- email: email
- )
+ context 'non GitLab users' do
+ shared_examples 'mailto links' do
+ it 'replaces them with mailto links' do
+ _, message_html = build_commit_message(
+ trailer: trailer,
+ name: FFaker::Name.name,
+ email: email
+ )
- doc = filter(message_html)
+ doc = filter(message_html)
- expect_to_have_mailto_link(doc, email: email, trailer: trailer)
+ expect_to_have_mailto_link_with_avatar(doc, email: email, trailer: trailer)
+ end
+ end
+
+ context 'when Gravatar is disabled' do
+ before do
+ stub_application_setting(gravatar_enabled: false)
+ end
+
+ it_behaves_like 'mailto links'
+ end
+
+ context 'when Gravatar is enabled' do
+ before do
+ stub_application_setting(gravatar_enabled: true)
+ end
+
+ it_behaves_like 'mailto links'
+ end
end
it 'multiple trailers in the same message' do
@@ -69,7 +89,7 @@ describe Banzai::Filter::CommitTrailersFilter do
doc = filter(message)
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
- expect_to_have_mailto_link(doc, email: email, trailer: different_trailer)
+ expect_to_have_mailto_link_with_avatar(doc, email: email, trailer: different_trailer)
end
context 'special names' do
@@ -90,7 +110,7 @@ describe Banzai::Filter::CommitTrailersFilter do
doc = filter(message_html)
- expect_to_have_mailto_link(doc, email: email, trailer: trailer)
+ expect_to_have_mailto_link_with_avatar(doc, email: email, trailer: trailer)
expect(doc.text).to match Regexp.escape(message)
end
end
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index ca76d6f0881..0e178b859c4 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -91,6 +91,12 @@ describe Banzai::Filter::GollumTagsFilter do
expect(doc.at_css('a').text).to eq 'link-text'
expect(doc.at_css('a')['href']).to eq expected_path
end
+
+ it "inside back ticks will be exempt from linkification" do
+ doc = filter('<code>[[link-in-backticks]]</code>', project_wiki: project_wiki)
+
+ expect(doc.at_css('code').text).to eq '[[link-in-backticks]]'
+ end
end
context 'table of contents' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 392905076dc..b30f3661e70 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -59,7 +59,7 @@ describe Banzai::Filter::LabelReferenceFilter do
describe 'label span element' do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
- expect(doc.css('a span').first.attr('class')).to eq 'label color-label has-tooltip'
+ expect(doc.css('a span').first.attr('class')).to eq 'badge color-label has-tooltip'
end
it 'includes a style attribute' do
diff --git a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb
index 726a3c1c83a..43b68e69131 100644
--- a/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb
+++ b/spec/lib/gitlab/auth/blocked_user_tracker_spec.rb
@@ -17,12 +17,8 @@ describe Gitlab::Auth::BlockedUserTracker do
end
context 'failed login due to blocked user' do
- let(:env) do
- {
- 'warden.options' => { message: User::BLOCKED_MESSAGE },
- described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } }
- }
- end
+ let(:base_env) { { 'warden.options' => { message: User::BLOCKED_MESSAGE } } }
+ let(:env) { base_env.merge(request_env) }
subject { described_class.log_if_user_blocked(env) }
@@ -30,23 +26,37 @@ describe Gitlab::Auth::BlockedUserTracker do
expect_any_instance_of(SystemHooksService).to receive(:execute_hooks_for).with(user, :failed_login)
end
- it 'logs a blocked user' do
- user.block!
+ context 'via GitLab login' do
+ let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'user' => { 'login' => user.username } } } }
- expect(subject).to be_truthy
- end
+ it 'logs a blocked user' do
+ user.block!
+
+ expect(subject).to be_truthy
+ end
- it 'logs a blocked user by e-mail' do
- user.block!
- env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email
+ it 'logs a blocked user by e-mail' do
+ user.block!
+ env[described_class::ACTIVE_RECORD_REQUEST_PARAMS]['user']['login'] = user.email
- expect(subject).to be_truthy
+ expect(subject).to be_truthy
+ end
end
- it 'logs a LDAP blocked user' do
- user.ldap_block!
+ context 'via LDAP login' do
+ let(:request_env) { { described_class::ACTIVE_RECORD_REQUEST_PARAMS => { 'username' => user.username } } }
+
+ it 'logs a blocked user' do
+ user.block!
+
+ expect(subject).to be_truthy
+ end
+
+ it 'logs a LDAP blocked user' do
+ user.ldap_block!
- expect(subject).to be_truthy
+ expect(subject).to be_truthy
+ end
end
end
end
diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb
index 6b251d824f7..eff21985108 100644
--- a/spec/lib/gitlab/auth/ldap/access_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/access_spec.rb
@@ -8,6 +8,7 @@ describe Gitlab::Auth::LDAP::Access do
describe '.allowed?' do
it 'updates the users `last_credential_check_at' do
+ allow(access).to receive(:update_user)
expect(access).to receive(:allowed?) { true }
expect(described_class).to receive(:open).and_yield(access)
@@ -16,12 +17,21 @@ describe Gitlab::Auth::LDAP::Access do
end
end
+ describe '#find_ldap_user' do
+ it 'finds a user by dn first' do
+ expect(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
+
+ access.find_ldap_user
+ end
+ end
+
describe '#allowed?' do
subject { access.allowed? }
context 'when the user cannot be found' do
before do
allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil)
end
it { is_expected.to be_falsey }
@@ -54,7 +64,7 @@ describe Gitlab::Auth::LDAP::Access do
end
end
- context 'and has no disabled flag in active diretory' do
+ context 'and has no disabled flag in active directory' do
before do
allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
end
@@ -100,6 +110,7 @@ describe Gitlab::Auth::LDAP::Access do
context 'when user cannot be found' do
before do
allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil)
end
it { is_expected.to be_falsey }
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 82587e2ba55..d3ab599d5a0 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::Auth::LDAP::Config do
end
it 'raises an error if a unknown provider is used' do
- expect { described_class.new 'unknown' }.to raise_error(RuntimeError)
+ expect { described_class.new 'unknown' }.to raise_error(described_class::InvalidProvider)
end
end
@@ -370,4 +370,38 @@ describe Gitlab::Auth::LDAP::Config do
})
end
end
+
+ describe '#base' do
+ context 'when the configured base is not normalized' do
+ it 'returns the normalized base' do
+ stub_ldap_config(options: { 'base' => 'DC=example, DC= com' })
+
+ expect(config.base).to eq('dc=example,dc=com')
+ end
+ end
+
+ context 'when the configured base is normalized' do
+ it 'returns the base unaltered' do
+ stub_ldap_config(options: { 'base' => 'dc=example,dc=com' })
+
+ expect(config.base).to eq('dc=example,dc=com')
+ end
+ end
+
+ context 'when the configured base is malformed' do
+ it 'returns the base unaltered' do
+ stub_ldap_config(options: { 'base' => 'invalid,dc=example,dc=com' })
+
+ expect(config.base).to eq('invalid,dc=example,dc=com')
+ end
+ end
+
+ context 'when the configured base is blank' do
+ it 'returns the base unaltered' do
+ stub_ldap_config(options: { 'base' => '' })
+
+ expect(config.base).to eq('')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index cab2169593a..44bb9d20e47 100644
--- a/spec/lib/gitlab/auth/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::Auth::LDAP::User do
+ include LdapHelpers
+
let(:ldap_user) { described_class.new(auth_hash) }
let(:gl_user) { ldap_user.gl_user }
let(:info) do
@@ -25,20 +27,20 @@ describe Gitlab::Auth::LDAP::User do
OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case)
end
- describe '#changed?' do
+ describe '#should_save?' do
it "marks existing ldap user as changed" do
create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
- expect(ldap_user.changed?).to be_truthy
+ expect(ldap_user.should_save?).to be_truthy
end
it "marks existing non-ldap user if the email matches as changed" do
create(:user, email: 'john@example.com')
- expect(ldap_user.changed?).to be_truthy
+ expect(ldap_user.should_save?).to be_truthy
end
it "does not mark existing ldap user as changed" do
create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
- expect(ldap_user.changed?).to be_falsey
+ expect(ldap_user.should_save?).to be_falsey
end
end
@@ -177,8 +179,7 @@ describe Gitlab::Auth::LDAP::User do
describe 'blocking' do
def configure_block(value)
- allow_any_instance_of(Gitlab::Auth::LDAP::Config)
- .to receive(:block_auto_created_users).and_return(value)
+ stub_ldap_config(block_auto_created_users: value)
end
context 'signup' do
diff --git a/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb
new file mode 100644
index 00000000000..528f1b4ec57
--- /dev/null
+++ b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::OAuth::IdentityLinker do
+ let(:user) { create(:user) }
+ let(:provider) { 'twitter' }
+ let(:uid) { user.email }
+ let(:oauth) { { 'provider' => provider, 'uid' => uid } }
+
+ subject { described_class.new(user, oauth) }
+
+ context 'linked identity exists' do
+ let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
+
+ it "doesn't create new identity" do
+ expect { subject.link }.not_to change { Identity.count }
+ end
+
+ it "sets #changed? to false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+ end
+
+ context 'identity already linked to different user' do
+ let!(:identity) { create(:identity, provider: provider, extern_uid: uid) }
+
+ it "#changed? returns false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+
+ it 'exposes error message' do
+ expect(subject.error_message).to eq 'Extern uid has already been taken'
+ end
+ end
+
+ context 'identity needs to be created' do
+ it 'creates linked identity' do
+ expect { subject.link }.to change { user.identities.count }
+ end
+
+ it 'sets identity provider' do
+ subject.link
+
+ expect(user.identities.last.provider).to eq provider
+ end
+
+ it 'sets identity extern_uid' do
+ subject.link
+
+ expect(user.identities.last.extern_uid).to eq uid
+ end
+
+ it 'sets #changed? to true' do
+ subject.link
+
+ expect(subject).to be_changed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/saml/identity_linker_spec.rb b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb
new file mode 100644
index 00000000000..f3305d574cc
--- /dev/null
+++ b/spec/lib/gitlab/auth/saml/identity_linker_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::Saml::IdentityLinker do
+ let(:user) { create(:user) }
+ let(:provider) { 'saml' }
+ let(:uid) { user.email }
+ let(:oauth) { { 'provider' => provider, 'uid' => uid } }
+
+ subject { described_class.new(user, oauth) }
+
+ context 'linked identity exists' do
+ let!(:identity) { user.identities.create!(provider: provider, extern_uid: uid) }
+
+ it "doesn't create new identity" do
+ expect { subject.link }.not_to change { Identity.count }
+ end
+
+ it "sets #changed? to false" do
+ subject.link
+
+ expect(subject).not_to be_changed
+ end
+ end
+
+ context 'identity needs to be created' do
+ it 'creates linked identity' do
+ expect { subject.link }.to change { user.identities.count }
+ end
+
+ it 'sets identity provider' do
+ subject.link
+
+ expect(user.identities.last.provider).to eq provider
+ end
+
+ it 'sets identity extern_uid' do
+ subject.link
+
+ expect(user.identities.last.extern_uid).to eq uid
+ end
+
+ it 'sets #changed? to true' do
+ subject.link
+
+ expect(subject).to be_changed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
new file mode 100644
index 00000000000..002ce776be9
--- /dev/null
+++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::UserAccessDeniedReason do
+ include TermsHelper
+ let(:user) { build(:user) }
+
+ let(:reason) { described_class.new(user) }
+
+ describe '#rejection_message' do
+ subject { reason.rejection_message }
+
+ context 'when a user is blocked' do
+ before do
+ user.block!
+ end
+
+ it { is_expected.to match /blocked/ }
+ end
+
+ context 'a user did not accept the enforced terms' do
+ before do
+ enforce_terms
+ end
+
+ it { is_expected.to match /must accept the Terms of Service/ }
+ it { is_expected.to include(user.username) }
+ end
+
+ context 'when the user is internal' do
+ let(:user) { User.ghost }
+
+ it { is_expected.to match /This action cannot be performed by internal users/ }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
new file mode 100644
index 00000000000..f8107dd40b9
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 20180420080616 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:stages) { table(:ci_stages) }
+ let(:jobs) { table(:ci_builds) }
+
+ before do
+ namespaces.create(id: 10, name: 'gitlab-org', path: 'gitlab-org')
+ projects.create!(id: 11, namespace_id: 10, name: 'gitlab', path: 'gitlab')
+ pipelines.create!(id: 12, project_id: 11, ref: 'master', sha: 'adf43c3a')
+
+ stages.create(id: 100, project_id: 11, pipeline_id: 12, name: 'build')
+ stages.create(id: 101, project_id: 11, pipeline_id: 12, name: 'test')
+
+ jobs.create!(id: 121, commit_id: 12, project_id: 11,
+ stage_idx: 2, stage_id: 100)
+ jobs.create!(id: 122, commit_id: 12, project_id: 11,
+ stage_idx: 2, stage_id: 100)
+ jobs.create!(id: 123, commit_id: 12, project_id: 11,
+ stage_idx: 10, stage_id: 100)
+ jobs.create!(id: 124, commit_id: 12, project_id: 11,
+ stage_idx: 3, stage_id: 101)
+ end
+
+ it 'correctly migrates stages indices' do
+ expect(stages.all.pluck(:position)).to all(be_nil)
+
+ described_class.new.perform(100, 101)
+
+ expect(stages.all.pluck(:position)).to eq [2, 3]
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_import_state_spec.rb b/spec/lib/gitlab/background_migration/populate_import_state_spec.rb
new file mode 100644
index 00000000000..f9952ee5163
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_import_state_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateImportState, :migration, schema: 20180502134117 do
+ let(:migration) { described_class.new }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:import_state) { table(:project_mirror_data) }
+
+ before do
+ namespaces.create(id: 1, name: 'gitlab-org', path: 'gitlab-org')
+
+ projects.create!(id: 1, namespace_id: 1, name: 'gitlab1',
+ path: 'gitlab1', import_error: "foo", import_status: :started,
+ import_url: generate(:url))
+ projects.create!(id: 2, namespace_id: 1, name: 'gitlab2', path: 'gitlab2',
+ import_status: :none, import_url: generate(:url))
+ projects.create!(id: 3, namespace_id: 1, name: 'gitlab3',
+ path: 'gitlab3', import_error: "bar", import_status: :failed,
+ import_url: generate(:url))
+
+ allow(BackgroundMigrationWorker).to receive(:perform_in)
+ end
+
+ it "creates new import_state records with project's import data" do
+ expect(projects.where.not(import_status: :none).count).to eq(2)
+
+ expect do
+ migration.perform(1, 3)
+ end.to change { import_state.all.count }.from(0).to(2)
+
+ expect(import_state.first.last_error).to eq("foo")
+ expect(import_state.last.last_error).to eq("bar")
+ expect(import_state.first.status).to eq("started")
+ expect(import_state.last.status).to eq("failed")
+ expect(projects.first.import_status).to eq("none")
+ expect(projects.last.import_status).to eq("none")
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/rollback_import_state_data_spec.rb b/spec/lib/gitlab/background_migration/rollback_import_state_data_spec.rb
new file mode 100644
index 00000000000..9f8c3bc220f
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/rollback_import_state_data_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::RollbackImportStateData, :migration, schema: 20180502134117 do
+ let(:migration) { described_class.new }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:import_state) { table(:project_mirror_data) }
+
+ before do
+ namespaces.create(id: 1, name: 'gitlab-org', path: 'gitlab-org')
+
+ projects.create!(id: 1, namespace_id: 1, name: 'gitlab1', import_url: generate(:url))
+ projects.create!(id: 2, namespace_id: 1, name: 'gitlab2', path: 'gitlab2', import_url: generate(:url))
+
+ import_state.create!(id: 1, project_id: 1, status: :started, last_error: "foo")
+ import_state.create!(id: 2, project_id: 2, status: :failed)
+
+ allow(BackgroundMigrationWorker).to receive(:perform_in)
+ end
+
+ it "creates new import_state records with project's import data" do
+ migration.perform(1, 2)
+
+ expect(projects.first.import_status).to eq("started")
+ expect(projects.second.import_status).to eq("failed")
+ expect(projects.first.import_error).to eq("foo")
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index eb4b9d8b12f..5c8a19a53bc 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
let!(:admin) { create(:admin) }
let!(:base_dir) { Dir.mktmpdir + '/' }
let(:bare_repository) { Gitlab::BareRepositoryImport::Repository.new(base_dir, File.join(base_dir, "#{project_path}.git")) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
subject(:importer) { described_class.new(admin, bare_repository) }
@@ -84,12 +85,14 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
importer.create_project_if_needed
project = Project.find_by_full_path(project_path)
- repo_path = File.join(project.repository_storage_path, project.disk_path + '.git')
+ repo_path = "#{project.disk_path}.git"
hook_path = File.join(repo_path, 'hooks')
- expect(File).to exist(repo_path)
- expect(File.symlink?(hook_path)).to be true
- expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
+ expect(gitlab_shell.exists?(project.repository_storage, repo_path)).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, hook_path)).to be(true)
+
+ full_hook_path = File.join(project.repository.path_to_repo, 'hooks')
+ expect(File.readlink(full_hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
end
context 'hashed storage enabled' do
@@ -144,8 +147,8 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
project = Project.find_by_full_path("#{admin.full_path}/#{project_path}")
- expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
- expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.git')).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
end
it 'moves an existing project to the correct path' do
@@ -155,7 +158,9 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
project = build(:project, :legacy_storage, :repository)
original_commit_count = project.repository.commit_count
- bare_repo = Gitlab::BareRepositoryImport::Repository.new(project.repository_storage_path, project.repository.path)
+ legacy_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
+
+ bare_repo = Gitlab::BareRepositoryImport::Repository.new(legacy_path, project.repository.path)
gitlab_importer = described_class.new(admin, bare_repo)
expect(gitlab_importer).to receive(:create_project).and_call_original
@@ -183,7 +188,7 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
project = Project.find_by_full_path(project_path)
- expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.wiki.git'))
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path + '.wiki.git')).to be(true)
end
end
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index 0dc3705825d..1504826c7a5 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -67,7 +67,7 @@ describe ::Gitlab::BareRepositoryImport::Repository do
end
after do
- gitlab_shell.remove_repository(root_path, hashed_path)
+ gitlab_shell.remove_repository(repository_storage, hashed_path)
end
subject { described_class.new(root_path, repo_path) }
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index ae5b31dc12d..c3f528dd6fc 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do
has_wiki?: false)
end
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
diff --git a/spec/lib/gitlab/build_access_spec.rb b/spec/lib/gitlab/build_access_spec.rb
new file mode 100644
index 00000000000..08f50bf4fac
--- /dev/null
+++ b/spec/lib/gitlab/build_access_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::BuildAccess do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ describe '#can_do_action' do
+ subject { described_class.new(user, project: project).can_do_action?(:download_code) }
+
+ context 'when the user can do an action on the project but cannot access git' do
+ before do
+ user.block!
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when the user cannot do an action on the project' do
+ it { is_expected.to be(false) }
+ 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 08718c382b9..83d39b82068 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -111,7 +111,15 @@ describe Gitlab::Ci::Config::Entry::Policy do
context 'when specifying invalid variables expressions token' do
let(:config) { { variables: ['$MY_VAR == 123'] } }
- it 'reports an error about invalid statement' do
+ it 'reports an error about invalid expression' do
+ expect(entry.errors).to include /invalid expression syntax/
+ end
+ end
+
+ context 'when using invalid variables expressions regexp' do
+ let(:config) { { variables: ['$MY_VAR =~ /some ( thing/'] } }
+
+ it 'reports an error about invalid expression' do
expect(entry.errors).to include /invalid expression syntax/
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 3ae7053a995..85d73e5c382 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -5,6 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
set(:user) { create(:user) }
let(:pipeline) { Ci::Pipeline.new }
+ let(:variables_attributes) do
+ [{ key: 'first', secret_value: 'world' },
+ { key: 'second', secret_value: 'second_world' }]
+ end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
source: :push,
@@ -15,7 +19,8 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
trigger_request: nil,
schedule: nil,
project: project,
- current_user: user)
+ current_user: user,
+ variables_attributes: variables_attributes)
end
let(:step) { described_class.new(pipeline, command) }
@@ -39,6 +44,8 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
expect(pipeline.tag).to be false
expect(pipeline.user).to eq user
expect(pipeline.project).to eq project
+ expect(pipeline.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
end
it 'sets a valid config source' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index dc12ba076bc..0edc3f315bb 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
context 'when pipeline is ready to be saved' do
before do
- pipeline.stages.build(name: 'test', project: project)
+ pipeline.stages.build(name: 'test', position: 0, project: project)
step.perform!
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 8312fa47cfa..4d7d6951a51 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -35,11 +35,6 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
it 'populates pipeline with stages' do
expect(pipeline.stages).to be_one
expect(pipeline.stages.first).not_to be_persisted
- end
-
- it 'populates pipeline with builds' do
- expect(pipeline.builds).to be_one
- expect(pipeline.builds.first).not_to be_persisted
expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted
end
@@ -151,8 +146,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
step.perform!
expect(pipeline.stages.size).to eq 1
- expect(pipeline.builds.size).to eq 1
- expect(pipeline.builds.first.name).to eq 'rspec'
+ expect(pipeline.stages.first.builds.size).to eq 1
+ expect(pipeline.stages.first.builds.first.name).to eq 'rspec'
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
new file mode 100644
index 00000000000..49e5af52f4d
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -0,0 +1,80 @@
+require 'fast_spec_helper'
+require_dependency 're2'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('=~', left, right))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns false when left and right do not match' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('something'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'returns true when left and right match' do
+ allow(left).to receive(:evaluate).and_return('my-awesome-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'supports matching against a nil value' do
+ allow(left).to receive(:evaluate).and_return(nil)
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('pattern'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'supports multiline strings' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('text-string'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'supports regexp flags' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My AWESOME content
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
new file mode 100644
index 00000000000..3ebc2e94727
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
@@ -0,0 +1,96 @@
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('/.*/'))
+ .to be_a(described_class)
+ end
+
+ it 'raises an error if pattern is invalid' do
+ expect { described_class.build('/ some ( thin/i') }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError)
+ end
+ end
+
+ describe '.type' do
+ it 'is a value lexeme' do
+ expect(described_class.type).to eq :value
+ end
+ end
+
+ describe '.scan' do
+ it 'correctly identifies a pattern token' do
+ scanner = StringScanner.new('/pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('pattern')
+ end
+
+ it 'is a greedy scanner for regexp boundaries' do
+ scanner = StringScanner.new('/some .* / pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
+ end
+
+ it 'does not allow to use an empty pattern' do
+ scanner = StringScanner.new(%(//))
+
+ token = described_class.scan(scanner)
+
+ expect(token).to be_nil
+ end
+
+ it 'support single flag' do
+ scanner = StringScanner.new('/pattern/i')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('(?i)pattern')
+ end
+
+ it 'support multiple flags' do
+ scanner = StringScanner.new('/pattern/im')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('(?im)pattern')
+ end
+
+ it 'does not support arbitrary flags' do
+ scanner = StringScanner.new('/pattern/x')
+
+ token = described_class.scan(scanner)
+
+ expect(token).to be_nil
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns a regular expression' do
+ regexp = described_class.new('/abc/')
+
+ expect(regexp.evaluate).to eq Gitlab::UntrustedRegexp.new('abc')
+ end
+
+ it 'raises error if evaluated regexp is not valid' do
+ allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true)
+
+ regexp = described_class.new('/invalid ( .*/')
+
+ expect { regexp.evaluate }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::RuntimeError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
index 230ceeb07f8..3f11b3f7673 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
end
describe '#tokens' do
- it 'tokenss single value' do
+ it 'returns single value' do
tokens = described_class.new('$VARIABLE').tokens
expect(tokens).to be_one
@@ -20,14 +20,14 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens).to all(be_an_instance_of(token_class))
end
- it 'tokenss multiple values of the same token' do
+ it 'returns multiple values of the same token' do
tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens
expect(tokens.size).to eq 2
expect(tokens).to all(be_an_instance_of(token_class))
end
- it 'tokenss multiple values with different tokens' do
+ it 'returns multiple values with different tokens' do
tokens = described_class.new('$VARIABLE "text" "value"').tokens
expect(tokens.size).to eq 3
@@ -36,7 +36,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect(tokens.third.value).to eq '"value"'
end
- it 'tokenss tokens and operators' do
+ it 'returns tokens and operators' do
tokens = described_class.new('$VARIABLE == "text"').tokens
expect(tokens.size).to eq 3
diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
index e8e6f585310..2b78b1dd4a7 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 6685bf5385b..11e73294f18 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -1,4 +1,5 @@
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'rspec-parameterized'
describe Gitlab::Ci::Pipeline::Expression::Statement do
subject do
@@ -36,7 +37,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
'== "123"', # invalid left side
'"some string"', # only string provided
'$VAR ==', # invalid right side
- '12345', # unknown syntax
+ 'null', # missing lexemes
'' # empty statement
]
@@ -44,7 +45,7 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context "when expression grammar is #{syntax.inspect}" do
let(:text) { syntax }
- it 'aises a statement error exception' do
+ it 'raises a statement error exception' do
expect { subject.parse_tree }
.to raise_error described_class::StatementError
end
@@ -82,48 +83,66 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
describe '#evaluate' do
- statements = [
- ['$PRESENT_VARIABLE == "my variable"', true],
- ["$PRESENT_VARIABLE == 'my variable'", true],
- ['"my variable" == $PRESENT_VARIABLE', true],
- ['$PRESENT_VARIABLE == null', false],
- ['$EMPTY_VARIABLE == null', false],
- ['"" == $EMPTY_VARIABLE', true],
- ['$EMPTY_VARIABLE', ''],
- ['$UNDEFINED_VARIABLE == null', true],
- ['null == $UNDEFINED_VARIABLE', true],
- ['$PRESENT_VARIABLE', 'my variable'],
- ['$UNDEFINED_VARIABLE', nil]
- ]
-
- statements.each do |expression, value|
- context "when using expression `#{expression}`" do
- let(:text) { expression }
-
- it "evaluates to `#{value.inspect}`" do
- expect(subject.evaluate).to eq value
- end
+ using RSpec::Parameterized::TableSyntax
+
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ '"my variable" == $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE == null' | false
+ '$EMPTY_VARIABLE == null' | false
+ '"" == $EMPTY_VARIABLE' | true
+ '$EMPTY_VARIABLE' | ''
+ '$UNDEFINED_VARIABLE == null' | true
+ 'null == $UNDEFINED_VARIABLE' | true
+ '$PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | true
+ "$PRESENT_VARIABLE =~ /^var.*/" | false
+ "$EMPTY_VARIABLE =~ /var.*/" | false
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ "$PRESENT_VARIABLE =~ /VAR.*/i" | true
+ end
+
+ with_them do
+ let(:text) { expression }
+
+ it "evaluates to `#{params[:value].inspect}`" do
+ expect(subject.evaluate).to eq value
end
end
end
describe '#truthful?' do
- statements = [
- ['$PRESENT_VARIABLE == "my variable"', true],
- ["$PRESENT_VARIABLE == 'no match'", false],
- ['$UNDEFINED_VARIABLE == null', true],
- ['$PRESENT_VARIABLE', true],
- ['$UNDEFINED_VARIABLE', false],
- ['$EMPTY_VARIABLE', false]
- ]
-
- statements.each do |expression, value|
- context "when using expression `#{expression}`" do
- let(:text) { expression }
-
- it "returns `#{value.inspect}`" do
- expect(subject.truthful?).to eq value
- end
+ using RSpec::Parameterized::TableSyntax
+
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ "$PRESENT_VARIABLE == 'no match'" | false
+ '$UNDEFINED_VARIABLE == null' | true
+ '$PRESENT_VARIABLE' | true
+ '$UNDEFINED_VARIABLE' | false
+ '$EMPTY_VARIABLE' | false
+ '$INVALID = 1' | false
+ "$PRESENT_VARIABLE =~ /var.*/" | true
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ end
+
+ with_them do
+ let(:text) { expression }
+
+ it "returns `#{params[:value].inspect}`" do
+ expect(subject.truthful?).to eq value
+ end
+ end
+
+ context 'when evaluating expression raises an error' do
+ let(:text) { '$PRESENT_VARIABLE' }
+
+ it 'returns false' do
+ allow(subject).to receive(:evaluate)
+ .and_raise(described_class::StatementError)
+
+ expect(subject.truthful?).to be_falsey
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
index 6d7453f0de5..cedfe270f9d 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Token do
let(:value) { '$VARIABLE' }
diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
new file mode 100644
index 00000000000..477c7477df0
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Preloader do
+ describe '.preload' do
+ it 'preloads the author of every pipeline commit' do
+ commit = double(:commit)
+ pipeline = double(:pipeline, commit: commit)
+
+ expect(commit)
+ .to receive(:lazy_author)
+
+ expect(pipeline)
+ .to receive(:number_of_warnings)
+
+ described_class.preload([pipeline])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index eb1b285c7bd..05ce3412fd8 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -24,7 +24,8 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
describe '#attributes' do
it 'returns hash attributes of a stage' do
expect(subject.attributes).to be_a Hash
- expect(subject.attributes).to include(:name, :project)
+ expect(subject.attributes)
+ .to include(:name, :position, :pipeline, :project)
end
end
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f128c1d4ca4..e2bb378f663 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Play do
let(:user) { create(:user) }
- let(:project) { build.project }
- let(:build) { create(:ci_build, :manual) }
+ let(:project) { create(:project, :stubbed_repository) }
+ let(:build) { create(:ci_build, :manual, project: project) }
let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
subject { described_class.new(status) }
@@ -46,6 +46,8 @@ describe Gitlab::Ci::Status::Build::Play do
context 'when user can not push to the branch' do
before do
build.project.add_developer(user)
+ create(:protected_branch, :masters_can_push,
+ name: build.ref, project: project)
end
it { is_expected.not_to have_action }
diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
new file mode 100644
index 00000000000..6259b952add
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb
@@ -0,0 +1,383 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do
+ include ChunkedIOHelpers
+
+ set(:build) { create(:ci_build, :running) }
+ let(:chunked_io) { described_class.new(build) }
+
+ before do
+ stub_feature_flags(ci_enable_live_trace: true)
+ end
+
+ context "#initialize" do
+ context 'when a chunk exists' do
+ before do
+ build.trace.set('ABC')
+ end
+
+ it { expect(chunked_io.size).to eq(3) }
+ end
+
+ context 'when two chunks exist' do
+ before do
+ stub_buffer_size(4)
+ build.trace.set('ABCDEF')
+ end
+
+ it { expect(chunked_io.size).to eq(6) }
+ end
+
+ context 'when no chunks exists' do
+ it { expect(chunked_io.size).to eq(0) }
+ end
+ end
+
+ context "#seek" do
+ subject { chunked_io.seek(pos, where) }
+
+ before do
+ build.trace.set(sample_trace_raw)
+ end
+
+ context 'when moves pos to end of the file' do
+ let(:pos) { 0 }
+ let(:where) { IO::SEEK_END }
+
+ it { is_expected.to eq(sample_trace_raw.bytesize) }
+ end
+
+ context 'when moves pos to middle of the file' do
+ let(:pos) { sample_trace_raw.bytesize / 2 }
+ let(:where) { IO::SEEK_SET }
+
+ it { is_expected.to eq(pos) }
+ end
+
+ context 'when moves pos around' do
+ it 'matches the result' do
+ expect(chunked_io.seek(0)).to eq(0)
+ expect(chunked_io.seek(100, IO::SEEK_CUR)).to eq(100)
+ expect { chunked_io.seek(sample_trace_raw.bytesize + 1, IO::SEEK_CUR) }
+ .to raise_error('new position is outside of file')
+ end
+ end
+ end
+
+ context "#eof?" do
+ subject { chunked_io.eof? }
+
+ before do
+ build.trace.set(sample_trace_raw)
+ end
+
+ context 'when current pos is at end of the file' do
+ before do
+ chunked_io.seek(sample_trace_raw.bytesize, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when current pos is not at end of the file' do
+ before do
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context "#each_line" do
+ let(:string_io) { StringIO.new(sample_trace_raw) }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'yields lines' do
+ expect { |b| chunked_io.each_line(&b) }
+ .to yield_successive_args(*string_io.each_line.to_a)
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'calls get_chunk only once' do
+ expect_any_instance_of(Gitlab::Ci::Trace::ChunkedIO)
+ .to receive(:current_chunk).once.and_call_original
+
+ chunked_io.each_line { |line| }
+ end
+ end
+ end
+
+ context "#read" do
+ subject { chunked_io.read(length) }
+
+ context 'when read the whole size' do
+ let(:length) { nil }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it { is_expected.to eq(sample_trace_raw) }
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it { is_expected.to eq(sample_trace_raw) }
+ end
+ end
+
+ context 'when read only first 100 bytes' do
+ let(:length) { 100 }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw.byteslice(0, length))
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw.byteslice(0, length))
+ end
+ end
+ end
+
+ context 'when tries to read oversize' do
+ let(:length) { sample_trace_raw.bytesize + 1000 }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw)
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(sample_trace_raw)
+ end
+ end
+ end
+
+ context 'when tries to read 0 bytes' do
+ let(:length) { 0 }
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+ end
+ end
+
+ context "#readline" do
+ subject { chunked_io.readline }
+
+ let(:string_io) { StringIO.new(sample_trace_raw) }
+
+ shared_examples 'all line matching' do
+ it do
+ (0...sample_trace_raw.lines.count).each do
+ expect(chunked_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when pos is at middle of the file' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+
+ chunked_io.seek(chunked_io.size / 2)
+ string_io.seek(string_io.size / 2)
+ end
+
+ it 'reads from pos' do
+ expect(chunked_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ context "#write" do
+ subject { chunked_io.write(data) }
+
+ let(:data) { sample_trace_raw }
+
+ context 'when data does not exist' do
+ shared_examples 'writes a trace' do
+ it do
+ is_expected.to eq(data.bytesize)
+
+ chunked_io.seek(0, IO::SEEK_SET)
+ expect(chunked_io.read).to eq(data)
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(data.bytesize / 2)
+ end
+
+ it_behaves_like 'writes a trace'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(data.bytesize * 2)
+ end
+
+ it_behaves_like 'writes a trace'
+ end
+ end
+
+ context 'when data already exists' do
+ let(:exist_data) { 'exist data' }
+
+ shared_examples 'appends a trace' do
+ it do
+ chunked_io.seek(0, IO::SEEK_END)
+ is_expected.to eq(data.bytesize)
+
+ chunked_io.seek(0, IO::SEEK_SET)
+ expect(chunked_io.read).to eq(exist_data + data)
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(exist_data)
+ end
+
+ it_behaves_like 'appends a trace'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(exist_data)
+ end
+
+ it_behaves_like 'appends a trace'
+ end
+ end
+ end
+
+ context "#truncate" do
+ let(:offset) { 10 }
+
+ context 'when data does not exist' do
+ shared_examples 'truncates a trace' do
+ it do
+ chunked_io.truncate(offset)
+
+ chunked_io.seek(0, IO::SEEK_SET)
+ expect(chunked_io.read).to eq(sample_trace_raw.byteslice(0, offset))
+ end
+ end
+
+ context 'when buffer size is smaller than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize / 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'truncates a trace'
+ end
+
+ context 'when buffer size is larger than file size' do
+ before do
+ stub_buffer_size(sample_trace_raw.bytesize * 2)
+ build.trace.set(sample_trace_raw)
+ end
+
+ it_behaves_like 'truncates a trace'
+ end
+ end
+ end
+
+ context "#destroy!" do
+ subject { chunked_io.destroy! }
+
+ before do
+ build.trace.set(sample_trace_raw)
+ end
+
+ it 'deletes' do
+ expect { subject }.to change { chunked_io.size }
+ .from(sample_trace_raw.bytesize).to(0)
+
+ expect(Ci::BuildTraceChunk.where(build: build).count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index e5555546fa8..4f49958dd33 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -1,6 +1,12 @@
require 'spec_helper'
-describe Gitlab::Ci::Trace::Stream do
+describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
+ set(:build) { create(:ci_build, :running) }
+
+ before do
+ stub_feature_flags(ci_enable_live_trace: true)
+ end
+
describe 'delegates' do
subject { described_class.new { nil } }
@@ -11,337 +17,470 @@ describe Gitlab::Ci::Trace::Stream do
it { is_expected.to delegate_method(:path).to(:stream) }
it { is_expected.to delegate_method(:truncate).to(:stream) }
it { is_expected.to delegate_method(:valid?).to(:stream).as(:present?) }
- it { is_expected.to delegate_method(:file?).to(:path).as(:present?) }
end
describe '#limit' do
- let(:stream) do
- described_class.new do
- StringIO.new((1..8).to_a.join("\n"))
+ shared_examples_for 'limits' do
+ it 'if size is larger we start from beginning' do
+ stream.limit(20)
+
+ expect(stream.tell).to eq(0)
end
- end
- it 'if size is larger we start from beginning' do
- stream.limit(20)
+ it 'if size is smaller we start from the end' do
+ stream.limit(2)
- expect(stream.tell).to eq(0)
- end
+ expect(stream.raw).to eq("8")
+ end
- it 'if size is smaller we start from the end' do
- stream.limit(2)
+ context 'when the trace contains ANSI sequence and Unicode' do
+ let(:stream) do
+ described_class.new do
+ File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
+ end
+ end
- expect(stream.raw).to eq("8")
- end
+ it 'forwards to the next linefeed, case 1' do
+ stream.limit(7)
- context 'when the trace contains ANSI sequence and Unicode' do
- let(:stream) do
- described_class.new do
- File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
+ result = stream.raw
+
+ expect(result).to eq('')
+ expect(result.encoding).to eq(Encoding.default_external)
end
- end
- it 'forwards to the next linefeed, case 1' do
- stream.limit(7)
+ it 'forwards to the next linefeed, case 2' do
+ stream.limit(29)
- result = stream.raw
+ result = stream.raw
- expect(result).to eq('')
- expect(result.encoding).to eq(Encoding.default_external)
- end
+ expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
- it 'forwards to the next linefeed, case 2' do
- stream.limit(29)
+ # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
+ it 'reads in binary, output as Encoding.default_external' do
+ stream.limit(52)
- result = stream.raw
+ result = stream.html
- expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
- expect(result.encoding).to eq(Encoding.default_external)
+ expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
end
+ end
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
- it 'reads in binary, output as Encoding.default_external' do
- stream.limit(52)
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new((1..8).to_a.join("\n"))
+ end
+ end
- result = stream.html
+ it_behaves_like 'limits'
+ end
- expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
- expect(result.encoding).to eq(Encoding.default_external)
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write((1..8).to_a.join("\n"))
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'limits'
end
end
describe '#append' do
- let(:tempfile) { Tempfile.new }
+ shared_examples_for 'appends' do
+ it "truncates and append content" do
+ stream.append("89", 4)
+ stream.seek(0)
- let(:stream) do
- described_class.new do
- tempfile.write("12345678")
- tempfile.rewind
- tempfile
+ expect(stream.size).to eq(6)
+ expect(stream.raw).to eq("123489")
end
- end
- after do
- tempfile.unlink
- end
+ it 'appends in binary mode' do
+ '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
+ stream.append(byte, offset)
+ end
- it "truncates and append content" do
- stream.append("89", 4)
- stream.seek(0)
+ stream.seek(0)
- expect(stream.size).to eq(6)
- expect(stream.raw).to eq("123489")
+ expect(stream.size).to eq(4)
+ expect(stream.raw).to eq('😺')
+ end
end
- it 'appends in binary mode' do
- '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
- stream.append(byte, offset)
+ context 'when stream is Tempfile' do
+ let(:tempfile) { Tempfile.new }
+
+ let(:stream) do
+ described_class.new do
+ tempfile.write("12345678")
+ tempfile.rewind
+ tempfile
+ end
+ end
+
+ after do
+ tempfile.unlink
end
- stream.seek(0)
+ it_behaves_like 'appends'
+ end
- expect(stream.size).to eq(4)
- expect(stream.raw).to eq('😺')
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write('12345678')
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
+ end
+
+ it_behaves_like 'appends'
end
end
describe '#set' do
- let(:stream) do
- described_class.new do
- StringIO.new("12345678")
+ shared_examples_for 'sets' do
+ before do
+ stream.set("8901")
+ end
+
+ it "overwrite content" do
+ stream.seek(0)
+
+ expect(stream.size).to eq(4)
+ expect(stream.raw).to eq("8901")
end
end
- before do
- stream.set("8901")
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new("12345678")
+ end
+ end
+
+ it_behaves_like 'sets'
end
- it "overwrite content" do
- stream.seek(0)
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write('12345678')
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
+ end
- expect(stream.size).to eq(4)
- expect(stream.raw).to eq("8901")
+ it_behaves_like 'sets'
end
end
describe '#raw' do
- let(:path) { __FILE__ }
- let(:lines) { File.readlines(path) }
- let(:stream) do
- described_class.new do
- File.open(path)
+ shared_examples_for 'sets' do
+ it 'returns all contents if last_lines is not specified' do
+ result = stream.raw
+
+ expect(result).to eq(lines.join)
+ expect(result.encoding).to eq(Encoding.default_external)
end
- end
- it 'returns all contents if last_lines is not specified' do
- result = stream.raw
+ context 'limit max lines' do
+ before do
+ # specifying BUFFER_SIZE forces to seek backwards
+ allow(described_class).to receive(:BUFFER_SIZE)
+ .and_return(2)
+ end
- expect(result).to eq(lines.join)
- expect(result.encoding).to eq(Encoding.default_external)
- end
+ it 'returns last few lines' do
+ result = stream.raw(last_lines: 2)
- context 'limit max lines' do
- before do
- # specifying BUFFER_SIZE forces to seek backwards
- allow(described_class).to receive(:BUFFER_SIZE)
- .and_return(2)
+ expect(result).to eq(lines.last(2).join)
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
+
+ it 'returns everything if trying to get too many lines' do
+ result = stream.raw(last_lines: lines.size * 2)
+
+ expect(result).to eq(lines.join)
+ expect(result.encoding).to eq(Encoding.default_external)
+ end
end
+ end
- it 'returns last few lines' do
- result = stream.raw(last_lines: 2)
+ let(:path) { __FILE__ }
+ let(:lines) { File.readlines(path) }
- expect(result).to eq(lines.last(2).join)
- expect(result.encoding).to eq(Encoding.default_external)
+ context 'when stream is File' do
+ let(:stream) do
+ described_class.new do
+ File.open(path)
+ end
end
- it 'returns everything if trying to get too many lines' do
- result = stream.raw(last_lines: lines.size * 2)
+ it_behaves_like 'sets'
+ end
- expect(result).to eq(lines.join)
- expect(result.encoding).to eq(Encoding.default_external)
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write(File.binread(path))
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'sets'
end
end
describe '#html_with_state' do
- let(:stream) do
- described_class.new do
- StringIO.new("1234")
+ shared_examples_for 'html_with_states' do
+ it 'returns html content with state' do
+ result = stream.html_with_state
+
+ expect(result.html).to eq("1234")
end
- end
- it 'returns html content with state' do
- result = stream.html_with_state
+ context 'follow-up state' do
+ let!(:last_result) { stream.html_with_state }
- expect(result.html).to eq("1234")
- end
+ before do
+ stream.append("5678", 4)
+ stream.seek(0)
+ end
- context 'follow-up state' do
- let!(:last_result) { stream.html_with_state }
+ it "returns appended trace" do
+ result = stream.html_with_state(last_result.state)
- before do
- stream.append("5678", 4)
- stream.seek(0)
+ expect(result.append).to be_truthy
+ expect(result.html).to eq("5678")
+ end
+ end
+ end
+
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new("1234")
+ end
end
- it "returns appended trace" do
- result = stream.html_with_state(last_result.state)
+ it_behaves_like 'html_with_states'
+ end
- expect(result.append).to be_truthy
- expect(result.html).to eq("5678")
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write("1234")
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'html_with_states'
end
end
describe '#html' do
- let(:stream) do
- described_class.new do
- StringIO.new("12\n34\n56")
+ shared_examples_for 'htmls' do
+ it "returns html" do
+ expect(stream.html).to eq("12<br>34<br>56")
+ end
+
+ it "returns html for last line only" do
+ expect(stream.html(last_lines: 1)).to eq("56")
end
end
- it "returns html" do
- expect(stream.html).to eq("12<br>34<br>56")
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new("12\n34\n56")
+ end
+ end
+
+ it_behaves_like 'htmls'
end
- it "returns html for last line only" do
- expect(stream.html(last_lines: 1)).to eq("56")
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write("12\n34\n56")
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
+ end
+
+ it_behaves_like 'htmls'
end
end
describe '#extract_coverage' do
- let(:stream) do
- described_class.new do
- StringIO.new(data)
- end
- end
+ shared_examples_for 'extract_coverages' do
+ context 'valid content & regex' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- subject { stream.extract_coverage(regex) }
+ it { is_expected.to eq("98.29") }
+ end
- context 'valid content & regex' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'valid content & bad regex' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
+ let(:regex) { 'very covered' }
- it { is_expected.to eq("98.29") }
- end
+ it { is_expected.to be_nil }
+ end
- context 'valid content & bad regex' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
- let(:regex) { 'very covered' }
+ context 'no coverage content & regex' do
+ let(:data) { 'No coverage for today :sad:' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- it { is_expected.to be_nil }
- end
+ it { is_expected.to be_nil }
+ end
- context 'no coverage content & regex' do
- let(:data) { 'No coverage for today :sad:' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'multiple results in content & regex' do
+ let(:data) do
+ <<~HEREDOC
+ (98.39%) covered
+ (98.29%) covered
+ HEREDOC
+ end
- it { is_expected.to be_nil }
- end
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- context 'multiple results in content & regex' do
- let(:data) do
- <<~HEREDOC
- (98.39%) covered
- (98.29%) covered
- HEREDOC
+ it 'returns the last matched coverage' do
+ is_expected.to eq("98.29")
+ end
end
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'when BUFFER_SIZE is smaller than stream.size' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- it 'returns the last matched coverage' do
- is_expected.to eq("98.29")
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ end
+
+ it { is_expected.to eq("98.29") }
end
- end
- context 'when BUFFER_SIZE is smaller than stream.size' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'when regex is multi-byte char' do
+ let(:data) { '95.0 ゴッドファット\n' }
+ let(:regex) { '\d+\.\d+ ゴッドファット' }
- before do
- stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ end
+
+ it { is_expected.to eq('95.0') }
end
- it { is_expected.to eq("98.29") }
- end
+ context 'when BUFFER_SIZE is equal to stream.size' do
+ let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
+ let(:regex) { '\(\d+.\d+\%\) covered' }
- context 'when regex is multi-byte char' do
- let(:data) { '95.0 ゴッドファット\n' }
- let(:regex) { '\d+\.\d+ ゴッドファット' }
+ before do
+ stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
+ end
- before do
- stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
+ it { is_expected.to eq("98.29") }
end
- it { is_expected.to eq('95.0') }
- end
-
- context 'when BUFFER_SIZE is equal to stream.size' do
- let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
- let(:regex) { '\(\d+.\d+\%\) covered' }
+ context 'using a regex capture' do
+ let(:data) { 'TOTAL 9926 3489 65%' }
+ let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
- before do
- stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
+ it { is_expected.to eq("65") }
end
- it { is_expected.to eq("98.29") }
- end
+ context 'malicious regexp' do
+ let(:data) { malicious_text }
+ let(:regex) { malicious_regexp }
- context 'using a regex capture' do
- let(:data) { 'TOTAL 9926 3489 65%' }
- let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
+ include_examples 'malicious regexp'
+ end
- it { is_expected.to eq("65") }
- end
+ context 'multi-line data with rooted regexp' do
+ let(:data) { "\n65%\n" }
+ let(:regex) { '^(\d+)\%$' }
- context 'malicious regexp' do
- let(:data) { malicious_text }
- let(:regex) { malicious_regexp }
+ it { is_expected.to eq('65') }
+ end
- include_examples 'malicious regexp'
- end
+ context 'long line' do
+ let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
+ let(:regex) { '\d+\%' }
- context 'multi-line data with rooted regexp' do
- let(:data) { "\n65%\n" }
- let(:regex) { '^(\d+)\%$' }
+ it { is_expected.to eq('100') }
+ end
- it { is_expected.to eq('65') }
- end
+ context 'many lines' do
+ let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
+ let(:regex) { '\d+\%' }
- context 'long line' do
- let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
- let(:regex) { '\d+\%' }
+ it { is_expected.to eq('100') }
+ end
- it { is_expected.to eq('100') }
- end
+ context 'empty regex' do
+ let(:data) { 'foo' }
+ let(:regex) { '' }
- context 'many lines' do
- let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
- let(:regex) { '\d+\%' }
+ it 'skips processing' do
+ expect(stream).not_to receive(:read)
- it { is_expected.to eq('100') }
- end
+ is_expected.to be_nil
+ end
+ end
- context 'empty regex' do
- let(:data) { 'foo' }
- let(:regex) { '' }
+ context 'nil regex' do
+ let(:data) { 'foo' }
+ let(:regex) { nil }
- it 'skips processing' do
- expect(stream).not_to receive(:read)
+ it 'skips processing' do
+ expect(stream).not_to receive(:read)
- is_expected.to be_nil
+ is_expected.to be_nil
+ end
end
end
- context 'nil regex' do
- let(:data) { 'foo' }
- let(:regex) { nil }
+ subject { stream.extract_coverage(regex) }
- it 'skips processing' do
- expect(stream).not_to receive(:read)
+ context 'when stream is StringIO' do
+ let(:stream) do
+ described_class.new do
+ StringIO.new(data)
+ end
+ end
+
+ it_behaves_like 'extract_coverages'
+ end
- is_expected.to be_nil
+ context 'when stream is ChunkedIO' do
+ let(:stream) do
+ described_class.new do
+ Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
+ chunked_io.write(data)
+ chunked_io.seek(0, IO::SEEK_SET)
+ end
+ end
end
+
+ it_behaves_like 'extract_coverages'
end
end
end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 6a9c6442282..e9d755c2021 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Trace do
+describe Gitlab::Ci::Trace, :clean_gitlab_redis_cache do
let(:build) { create(:ci_build) }
let(:trace) { described_class.new(build) }
@@ -9,552 +9,19 @@ describe Gitlab::Ci::Trace do
it { expect(trace).to delegate_method(:old_trace).to(:job) }
end
- describe '#html' do
+ context 'when live trace feature is disabled' do
before do
- trace.set("12\n34")
+ stub_feature_flags(ci_enable_live_trace: false)
end
- it "returns formatted html" do
- expect(trace.html).to eq("12<br>34")
- end
-
- it "returns last line of formatted html" do
- expect(trace.html(last_lines: 1)).to eq("34")
- end
- end
-
- describe '#raw' do
- before do
- trace.set("12\n34")
- end
-
- it "returns raw output" do
- expect(trace.raw).to eq("12\n34")
- end
-
- it "returns last line of raw output" do
- expect(trace.raw(last_lines: 1)).to eq("34")
- end
- end
-
- describe '#extract_coverage' do
- let(:regex) { '\(\d+.\d+\%\) covered' }
-
- context 'matching coverage' do
- before do
- trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
- end
-
- it "returns valid coverage" do
- expect(trace.extract_coverage(regex)).to eq("98.29")
- end
- end
-
- context 'no coverage' do
- before do
- trace.set('No coverage')
- end
-
- it 'returs nil' do
- expect(trace.extract_coverage(regex)).to be_nil
- end
- end
- end
-
- describe '#extract_sections' do
- let(:log) { 'No sections' }
- let(:sections) { trace.extract_sections }
-
- before do
- trace.set(log)
- end
-
- context 'no sections' do
- it 'returs []' do
- expect(trace.extract_sections).to eq([])
- end
- end
-
- context 'multiple sections available' do
- let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) }
- let(:sections_data) do
- [
- { name: 'prepare_script', lines: 2, duration: 3.seconds },
- { name: 'get_sources', lines: 4, duration: 1.second },
- { name: 'restore_cache', lines: 0, duration: 0.seconds },
- { name: 'download_artifacts', lines: 0, duration: 0.seconds },
- { name: 'build_script', lines: 2, duration: 1.second },
- { name: 'after_script', lines: 0, duration: 0.seconds },
- { name: 'archive_cache', lines: 0, duration: 0.seconds },
- { name: 'upload_artifacts', lines: 0, duration: 0.seconds }
- ]
- end
-
- it "returns valid sections" do
- expect(sections).not_to be_empty
- expect(sections.size).to eq(sections_data.size),
- "expected #{sections_data.size} sections, got #{sections.size}"
-
- buff = StringIO.new(log)
- sections.each_with_index do |s, i|
- expected = sections_data[i]
-
- expect(s[:name]).to eq(expected[:name])
- expect(s[:date_end] - s[:date_start]).to eq(expected[:duration])
-
- buff.seek(s[:byte_start], IO::SEEK_SET)
- length = s[:byte_end] - s[:byte_start]
- lines = buff.read(length).count("\n")
- expect(lines).to eq(expected[:lines])
- end
- end
- end
-
- context 'logs contains "section_start"' do
- let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
-
- it "returns only one section" do
- expect(sections).not_to be_empty
- expect(sections.size).to eq(1)
-
- section = sections[0]
- expect(section[:name]).to eq('a_section')
- expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section"
- end
- end
-
- context 'missing section_end' do
- let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
-
- it "returns no sections" do
- expect(sections).to be_empty
- end
- end
-
- context 'missing section_start' do
- let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
-
- it "returns no sections" do
- expect(sections).to be_empty
- end
- end
-
- context 'inverted section_start section_end' do
- let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
-
- it "returns no sections" do
- expect(sections).to be_empty
- end
- end
- end
-
- describe '#set' do
- before do
- trace.set("12")
- end
-
- it "returns trace" do
- expect(trace.raw).to eq("12")
- end
-
- context 'overwrite trace' do
- before do
- trace.set("34")
- end
-
- it "returns new trace" do
- expect(trace.raw).to eq("34")
- end
- end
-
- context 'runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- trace.set(token)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
-
- context 'hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- trace.set(token)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
+ it_behaves_like 'trace with disabled live trace feature'
end
- describe '#append' do
+ context 'when live trace feature is enabled' do
before do
- trace.set("1234")
- end
-
- it "returns correct trace" do
- expect(trace.append("56", 4)).to eq(6)
- expect(trace.raw).to eq("123456")
- end
-
- context 'tries to append trace at different offset' do
- it "fails with append" do
- expect(trace.append("56", 2)).to eq(-4)
- expect(trace.raw).to eq("1234")
- end
- end
-
- context 'runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- trace.append(token, 0)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
-
- context 'build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- trace.append(token, 0)
- end
-
- it "hides token" do
- expect(trace.raw).not_to include(token)
- end
- end
- end
-
- describe '#read' do
- shared_examples 'read successfully with IO' do
- it 'yields with source' do
- trace.read do |stream|
- expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
- expect(stream.stream).to be_a(IO)
- end
- end
- end
-
- shared_examples 'read successfully with StringIO' do
- it 'yields with source' do
- trace.read do |stream|
- expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
- expect(stream.stream).to be_a(StringIO)
- end
- end
- end
-
- shared_examples 'failed to read' do
- it 'yields without source' do
- trace.read do |stream|
- expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
- expect(stream.stream).to be_nil
- end
- end
- end
-
- context 'when trace artifact exists' do
- before do
- create(:ci_job_artifact, :trace, job: build)
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
- context 'when current_path (with project_id) exists' do
- before do
- expect(trace).to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
- context 'when current_path (with project_ci_id) exists' do
- before do
- expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
- context 'when db trace exists' do
- before do
- build.send(:write_attribute, :trace, "data")
- end
-
- it_behaves_like 'read successfully with StringIO'
- end
-
- context 'when no sources exist' do
- it_behaves_like 'failed to read'
- end
- end
-
- describe 'trace handling' do
- subject { trace.exist? }
-
- context 'trace does not exist' do
- it { expect(trace.exist?).to be(false) }
- end
-
- context 'when trace artifact exists' do
- before do
- create(:ci_job_artifact, :trace, job: build)
- end
-
- it { is_expected.to be_truthy }
-
- context 'when the trace artifact has been erased' do
- before do
- trace.erase!
- end
-
- it { is_expected.to be_falsy }
-
- it 'removes associations' do
- expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
- end
- end
- end
-
- context 'new trace path is used' do
- before do
- trace.send(:ensure_directory)
-
- File.open(trace.send(:default_path), "w") do |file|
- file.write("data")
- end
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
- end
-
- context 'deprecated path' do
- let(:path) { trace.send(:deprecated_path) }
-
- context 'with valid ci_id' do
- before do
- build.project.update(ci_id: 1000)
-
- FileUtils.mkdir_p(File.dirname(path))
-
- File.open(path, "w") do |file|
- file.write("data")
- end
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
- end
-
- context 'without valid ci_id' do
- it "does not return deprecated path" do
- expect(path).to be_nil
- end
- end
- end
-
- context 'stored in database' do
- before do
- build.send(:write_attribute, :trace, "data")
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
-
- it "returns database data" do
- expect(trace.raw).to eq("data")
- end
- end
- end
-
- describe '#archive!' do
- subject { trace.archive! }
-
- shared_examples 'archive trace file' do
- it do
- expect { subject }.to change { Ci::JobArtifact.count }.by(1)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace.file.exists?).to be_truthy
- expect(build.job_artifacts_trace.file.filename).to eq('job.log')
- expect(File.exist?(src_path)).to be_falsy
- expect(src_checksum)
- .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
- expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
- end
- end
-
- shared_examples 'source trace file stays intact' do |error:|
- it do
- expect { subject }.to raise_error(error)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace).to be_nil
- expect(File.exist?(src_path)).to be_truthy
- end
- end
-
- shared_examples 'archive trace in database' do
- it do
- expect { subject }.to change { Ci::JobArtifact.count }.by(1)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace.file.exists?).to be_truthy
- expect(build.job_artifacts_trace.file.filename).to eq('job.log')
- expect(build.old_trace).to be_nil
- expect(src_checksum)
- .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
- expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
- end
- end
-
- shared_examples 'source trace in database stays intact' do |error:|
- it do
- expect { subject }.to raise_error(error)
-
- build.reload
- expect(build.trace.exist?).to be_truthy
- expect(build.job_artifacts_trace).to be_nil
- expect(build.old_trace).to eq(trace_content)
- end
- end
-
- context 'when job does not have trace artifact' do
- context 'when trace file stored in default path' do
- let!(:build) { create(:ci_build, :success, :trace_live) }
- let!(:src_path) { trace.read { |s| s.path } }
- let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
-
- it_behaves_like 'archive trace file'
-
- context 'when failed to create clone file' do
- before do
- allow(IO).to receive(:copy_stream).and_return(0)
- end
-
- it_behaves_like 'source trace file stays intact', error: Gitlab::Ci::Trace::ArchiveError
- end
-
- context 'when failed to create job artifact record' do
- before do
- allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid
- end
- end
-
- context 'when trace is stored in database' do
- let(:build) { create(:ci_build, :success) }
- let(:trace_content) { 'Sample trace' }
- let!(:src_checksum) { Digest::SHA256.hexdigest(trace_content) }
-
- before do
- build.update_column(:trace, trace_content)
- end
-
- it_behaves_like 'archive trace in database'
-
- context 'when failed to create clone file' do
- before do
- allow(IO).to receive(:copy_stream).and_return(0)
- end
-
- it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError
- end
-
- context 'when failed to create job artifact record' do
- before do
- allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
- end
-
- context 'when there is a validation error on Ci::Build' do
- before do
- allow_any_instance_of(Ci::Build).to receive(:save).and_return(false)
- allow_any_instance_of(Ci::Build).to receive_message_chain(:errors, :full_messages)
- .and_return(%w[Error Error])
- end
-
- context "when erase old trace with 'save'" do
- before do
- build.send(:write_attribute, :trace, nil)
- build.save
- end
-
- it 'old trace is not deleted' do
- build.reload
- expect(build.trace.raw).to eq(trace_content)
- end
- end
-
- it_behaves_like 'archive trace in database'
- end
- end
+ stub_feature_flags(ci_enable_live_trace: true)
end
- context 'when job has trace artifact' do
- before do
- create(:ci_job_artifact, :trace, job: build)
- end
-
- it 'does not archive' do
- expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Already archived')
- expect(build.job_artifacts_trace.file.exists?).to be_truthy
- end
- end
-
- context 'when job is not finished yet' do
- let!(:build) { create(:ci_build, :running, :trace_live) }
-
- it 'does not archive' do
- expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Job is not finished yet')
- expect(build.trace.exist?).to be_truthy
- end
- end
+ it_behaves_like 'trace with enabled live trace feature'
end
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 4ddcbd7eb66..19028495f52 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -1,17 +1,15 @@
require 'spec_helper'
describe Gitlab::CurrentSettings do
- include StubENV
-
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
- describe '#current_application_settings' do
+ describe '#current_application_settings', :use_clean_rails_memory_store_caching do
it 'allows keys to be called directly' do
db_settings = create(:application_setting,
- home_page_url: 'http://mydomain.com',
- signup_enabled: false)
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
expect(described_class.home_page_url).to eq(db_settings.home_page_url)
expect(described_class.signup_enabled?).to be_falsey
@@ -19,46 +17,54 @@ describe Gitlab::CurrentSettings do
expect(described_class.metrics_sample_interval).to be(15)
end
- context 'with DB available' do
+ context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
- allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original
- allow(ActiveRecord::Base.connection).to receive(:table_exists?).with('application_settings').and_return(true)
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
end
- it 'attempts to use cached values first' do
- expect(ApplicationSetting).to receive(:cached)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
expect(described_class.current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).not_to be_persisted
end
+ end
- it 'falls back to DB if Redis returns an empty value' do
- expect(ApplicationSetting).to receive(:cached).and_return(nil)
- expect(ApplicationSetting).to receive(:last).and_call_original.twice
-
- expect(described_class.current_application_settings).to be_a(ApplicationSetting)
+ context 'with DB unavailable' do
+ before do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
end
- it 'falls back to DB if Redis fails' do
- db_settings = ApplicationSetting.create!(ApplicationSetting.defaults)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
- expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
+ end
- expect(described_class.current_application_settings).to eq(db_settings)
+ context 'with DB available' do
+ # This method returns the ::ApplicationSetting.defaults hash
+ # but with respect of custom attribute accessors of ApplicationSetting model
+ def settings_from_defaults
+ ar_wrapped_defaults = ::ApplicationSetting.build_from_defaults.attributes
+ ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys)
end
- it 'creates default ApplicationSettings if none are present' do
- expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
- expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
+ before do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
+ end
+ it 'creates default ApplicationSettings if none are present' do
settings = described_class.current_application_settings
expect(settings).to be_a(ApplicationSetting)
expect(settings).to be_persisted
- expect(settings).to have_attributes(ApplicationSetting.defaults)
+ expect(settings).to have_attributes(settings_from_defaults)
end
context 'with migrations pending' do
@@ -69,7 +75,7 @@ describe Gitlab::CurrentSettings do
it 'returns an in-memory ApplicationSetting object' do
settings = described_class.current_application_settings
- expect(settings).to be_a(OpenStruct)
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
end
@@ -81,7 +87,7 @@ describe Gitlab::CurrentSettings do
settings = described_class.current_application_settings
app_defaults = ApplicationSetting.last
- expect(settings).to be_a(OpenStruct)
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
expect(settings.home_page_url).to eq(db_settings.home_page_url)
expect(settings.signup_enabled?).to be_falsey
expect(settings.signup_enabled).to be_falsey
@@ -91,34 +97,29 @@ describe Gitlab::CurrentSettings do
settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
end
end
- end
-
- context 'with DB unavailable' do
- before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
- end
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).not_to receive(:last)
+ context 'when ApplicationSettings.current is present' do
+ it 'returns the existing application settings' do
+ expect(ApplicationSetting).to receive(:current).and_return(:current_settings)
- expect(described_class.current_application_settings).to be_a(OpenStruct)
+ expect(described_class.current_application_settings).to eq(:current_settings)
+ end
end
- end
- context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
+ context 'when the application_settings table does not exists' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid)
+
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).not_to receive(:last)
+ context 'when the application_settings table is not fully migrated' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError)
- expect(described_class.current_application_settings).to be_a(ApplicationSetting)
- expect(described_class.current_application_settings).not_to be_persisted
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
end
end
diff --git a/spec/lib/gitlab/data_builder/wiki_page_spec.rb b/spec/lib/gitlab/data_builder/wiki_page_spec.rb
index a776d888c47..9c8bdf4b032 100644
--- a/spec/lib/gitlab/data_builder/wiki_page_spec.rb
+++ b/spec/lib/gitlab/data_builder/wiki_page_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::DataBuilder::WikiPage do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository, :wiki_repo) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb
new file mode 100644
index 00000000000..407d9470785
--- /dev/null
+++ b/spec/lib/gitlab/database/count_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count do
+ before do
+ create_list(:project, 3)
+ create(:identity)
+ end
+
+ let(:models) { [Project, Identity] }
+
+ describe '.approximate_counts' do
+ context 'with MySQL' do
+ context 'when reltuples have not been updated' do
+ it 'counts all models the normal way' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+ end
+
+ context 'with PostgreSQL', :postgresql do
+ describe 'when reltuples have not been updated' do
+ it 'counts all models the normal way' do
+ expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({})
+
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+
+ describe 'no permission' do
+ it 'falls back to standard query' do
+ allow(described_class).to receive(:postgresql_estimate_query).and_raise(PG::InsufficientPrivilege)
+
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+
+ describe 'when some reltuples have been updated' do
+ it 'counts projects in the fast way' do
+ expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({ 'projects' => 3 })
+
+ expect(Project).not_to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+
+ describe 'when all reltuples have been updated' do
+ before do
+ ActiveRecord::Base.connection.execute('ANALYZE projects')
+ ActiveRecord::Base.connection.execute('ANALYZE identities')
+ end
+
+ it 'counts models with the standard way' do
+ expect(Project).not_to receive(:count)
+ expect(Identity).not_to receive(:count)
+
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 1fe1d3926ad..8ac36ae8bab 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::Database do
end
describe '.version' do
+ around do |example|
+ described_class.instance_variable_set(:@version, nil)
+ example.run
+ described_class.instance_variable_set(:@version, nil)
+ end
+
context "on mysql" do
it "extracts the version number" do
allow(described_class).to receive(:database_version)
@@ -49,6 +55,14 @@ describe Gitlab::Database do
expect(described_class.version).to eq '9.4.4'
end
end
+
+ it 'memoizes the result' do
+ count = ActiveRecord::QueryRecorder
+ .new { 2.times { described_class.version } }
+ .count
+
+ expect(count).to eq(1)
+ end
end
describe '.join_lateral_supported?' do
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0c2e18c268a..0588fe935c3 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -468,4 +468,58 @@ describe Gitlab::Diff::File do
end
end
end
+
+ describe '#diff_hunk' do
+ let(:raw_diff) do
+ <<EOS
+@@ -6,12 +6,18 @@ module Popen
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+- raise "System commands must be given as an array of strings"
++ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+- vars = { "PWD" => path }
+- options = { chdir: path }
++
++ vars = {
++ "PWD" => path
++ }
++
++ options = {
++ chdir: path
++ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+@@ -19,6 +25,7 @@ module Popen
+
+ @cmd_output = ""
+ @cmd_status = 0
++
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+EOS
+ end
+
+ it 'returns raw diff up to given line index' do
+ allow(diff_file).to receive(:raw_diff) { raw_diff }
+ diff_line = instance_double(Gitlab::Diff::Line, index: 5)
+
+ diff_hunk = <<EOS
+@@ -6,12 +6,18 @@ module Popen
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+- raise "System commands must be given as an array of strings"
++ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+EOS
+
+ expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk)
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index 6568a0b1bb0..154ab4b3856 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateIssueHandler do
include_context :email_shared_context
@@ -47,6 +46,20 @@ describe Gitlab::Email::Handler::CreateIssueHandler do
expect(issue.description).to eq('')
end
end
+
+ context "when there are quotes in email" do
+ let(:email_raw) { fixture_file("emails/valid_new_issue_with_quote.eml") }
+
+ it "creates a new issue" do
+ expect { receiver.execute }.to change { project.issues.count }.by(1)
+ issue = project.issues.last
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq('New Issue by email')
+ expect(issue.description).to include('reply by email')
+ expect(issue.description).to include('> this is a quote')
+ end
+ end
end
context "something is wrong" do
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 dc1a93367a4..43c6280f251 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
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateMergeRequestHandler do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 53899e00b53..950a7dd7d6c 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateNoteHandler do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
index 21796694f26..ce160e11de2 100644
--- a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::UnsubscribeHandler do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 59f43abf26d..0af978eced3 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'email_shared_blocks'
describe Gitlab::Email::Receiver do
include_context :email_shared_context
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index e21a998adfe..0989188f7ee 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -3,8 +3,8 @@ require "spec_helper"
# Inspired in great part by Discourse's Email::Receiver
describe Gitlab::Email::ReplyParser do
describe '#execute' do
- def test_parse_body(mail_string)
- described_class.new(Mail::Message.new(mail_string)).execute
+ def test_parse_body(mail_string, params = {})
+ described_class.new(Mail::Message.new(mail_string), params).execute
end
it "returns an empty string if the message is blank" do
@@ -212,5 +212,19 @@ describe Gitlab::Email::ReplyParser do
it "does not wrap links with no href in unnecessary brackets" do
expect(test_parse_body(fixture_file("emails/html_empty_link.eml"))).to eq("no brackets!")
end
+
+ it "does not trim reply if trim_reply option is false" do
+ expect(test_parse_body(fixture_file("emails/valid_new_issue_with_quote.eml"), { trim_reply: false }))
+ .to eq(
+ <<-BODY.strip_heredoc.chomp
+ The reply by email functionality should be extended to allow creating a new issue by email.
+ even when the email is forwarded to the project which may include lines that begin with ">"
+
+ there should be a quote below this line:
+
+ > this is a quote
+ BODY
+ )
+ end
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 67d898e787e..e2547ed0311 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -251,6 +251,26 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
+ describe '.batch_metadata' do
+ let(:blob_references) do
+ [
+ [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
+ [SeedRepo::Commit::ID, 'six']
+ ]
+ end
+
+ subject { described_class.batch_metadata(repository, blob_references) }
+
+ it 'returns an empty data attribute' do
+ first_blob, last_blob = subject
+
+ expect(first_blob.data).to be_blank
+ expect(first_blob.path).to eq("files/ruby/popen.rb")
+ expect(last_blob.data).to be_blank
+ expect(last_blob.path).to eq("six")
+ end
+ end
+
describe '.batch_lfs_pointers' do
let(:tree_object) { repository.rugged.rev_parse('master^{tree}') }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index a05feaac1ca..08c6d1e55e9 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -66,7 +66,8 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe "Commit info from gitaly commit" do
let(:subject) { "My commit".force_encoding('ASCII-8BIT') }
let(:body) { subject + "My body".force_encoding('ASCII-8BIT') }
- let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body) }
+ let(:body_size) { body.length }
+ let(:gitaly_commit) { build(:gitaly_commit, subject: subject, body: body, body_size: body_size) }
let(:id) { gitaly_commit.id }
let(:committer) { gitaly_commit.committer }
let(:author) { gitaly_commit.author }
@@ -83,10 +84,30 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(commit.committer_email).to eq(committer.email) }
it { expect(commit.parent_ids).to eq(gitaly_commit.parent_ids) }
- context 'no body' do
+ context 'body_size != body.size' do
let(:body) { "".force_encoding('ASCII-8BIT') }
- it { expect(commit.safe_message).to eq(subject) }
+ context 'zero body_size' do
+ it { expect(commit.safe_message).to eq(subject) }
+ end
+
+ context 'body_size less than threshold' do
+ let(:body_size) { 123 }
+
+ it 'fetches commit message seperately' do
+ expect(described_class).to receive(:get_message).with(repository, id)
+
+ commit.safe_message
+ end
+ end
+
+ context 'body_size greater than threshold' do
+ let(:body_size) { described_class::MAX_COMMIT_MESSAGE_DISPLAY_SIZE + 1 }
+
+ it 'returns the suject plus a notice about message size' do
+ expect(commit.safe_message).to eq("My commit\n\n--commit message is too big")
+ end
+ end
end
end
@@ -554,24 +575,10 @@ describe Gitlab::Git::Commit, seed_helper: true do
it_should_behave_like '#stats'
end
- describe '#to_diff' do
- subject { commit.to_diff }
-
- it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
- it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
- end
-
describe '#has_zero_stats?' do
it { expect(commit.has_zero_stats?).to eq(false) }
end
- describe '#to_patch' do
- subject { commit.to_patch }
-
- it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
- it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
- end
-
describe '#to_hash' do
let(:hash) { commit.to_hash }
subject { hash }
@@ -603,6 +610,35 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include("feature") }
end
+ describe '.get_message' do
+ let(:commit_ids) { %w[6d394385cf567f80a8fd85055db1ab4c5295806f cfe32cf61b73a0d5e9f13e774abde7ff789b1660] }
+
+ subject 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
+ end
+
+ context 'when Gitaly commit_messages feature is disabled', :disable_gitaly do
+ it_behaves_like 'getting commit messages'
+ end
+ end
+
def sample_commit_hash
{
author_email: "dmitriy.zaporozhets@gmail.com",
diff --git a/spec/lib/gitlab/git/committer_with_hooks_spec.rb b/spec/lib/gitlab/git/committer_with_hooks_spec.rb
new file mode 100644
index 00000000000..267056b96e6
--- /dev/null
+++ b/spec/lib/gitlab/git/committer_with_hooks_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe Gitlab::Git::CommitterWithHooks, seed_helper: true do
+ shared_examples 'calling wiki hooks' do
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:project_wiki) { ProjectWiki.new(project, user) }
+ let(:wiki) { project_wiki.wiki }
+ let(:options) do
+ {
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ email: user.email,
+ message: 'commit message'
+ }
+ end
+
+ subject { described_class.new(wiki, options) }
+
+ before do
+ project_wiki.create_page('home', 'test content')
+ end
+
+ shared_examples 'failing pre-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update')
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
+
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
+
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
+
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+
+ expect(current_rev).to eq find_current_rev
+ end
+ end
+
+ shared_examples 'failing update hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive')
+ end
+
+ it 'raises exception' do
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+ end
+
+ it 'does not create a new commit inside the repository' do
+ current_rev = find_current_rev
+
+ expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError)
+
+ expect(current_rev).to eq find_current_rev
+ end
+ end
+
+ shared_examples 'failing post-receive hook' do
+ before do
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, ''])
+ expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, ''])
+ end
+
+ it 'does not raise exception' do
+ expect { subject.commit }.not_to raise_error
+ end
+
+ it 'creates the commit' do
+ current_rev = find_current_rev
+
+ subject.commit
+
+ expect(current_rev).not_to eq find_current_rev
+ end
+ end
+
+ shared_examples 'when hooks call succceeds' do
+ let(:hook) { double(:hook) }
+
+ it 'calls the three hooks' do
+ expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
+ expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil])
+
+ subject.commit
+ end
+
+ it 'creates the commit' do
+ current_rev = find_current_rev
+
+ subject.commit
+
+ expect(current_rev).not_to eq find_current_rev
+ end
+ end
+
+ context 'when creating a page' do
+ before do
+ project_wiki.create_page('index', 'test content')
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ context 'when updating a page' do
+ before do
+ project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown)
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ context 'when deleting a page' do
+ before do
+ project_wiki.delete_page(find_page('home'))
+ end
+
+ it_behaves_like 'failing pre-receive hook'
+ it_behaves_like 'failing update hook'
+ it_behaves_like 'failing post-receive hook'
+ it_behaves_like 'when hooks call succceeds'
+ end
+
+ def find_current_rev
+ wiki.gollum_wiki.repo.commits.first&.sha
+ end
+
+ def find_page(name)
+ wiki.page(title: name)
+ end
+ end
+
+ # TODO: Uncomment once Gitaly updates the ruby vendor code
+ # context 'when Gitaly is enabled' do
+ # it_behaves_like 'calling wiki hooks'
+ # end
+
+ context 'when Gitaly is disabled', :skip_gitaly_mock do
+ it_behaves_like 'calling wiki hooks'
+ end
+end
diff --git a/spec/lib/gitlab/git/raw_diff_change_spec.rb b/spec/lib/gitlab/git/raw_diff_change_spec.rb
index eedde34534f..a0bb37fd84a 100644
--- a/spec/lib/gitlab/git/raw_diff_change_spec.rb
+++ b/spec/lib/gitlab/git/raw_diff_change_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Git::RawDiffChange do
expect(change.operation).to eq(:unknown)
expect(change.old_path).to be_blank
expect(change.new_path).to be_blank
- expect(change.blob_size).to be_blank
+ expect(change.blob_size).to eq(0)
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 5acf40ea5ce..af6a486ab20 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -234,59 +234,68 @@ describe Gitlab::Git::Repository, seed_helper: true do
it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names
end
- shared_examples 'archive check' do |extenstion|
- it { expect(metadata['ArchivePath']).to match(%r{tmp/gitlab-git-test.git/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}}) }
- it { expect(metadata['ArchivePath']).to end_with extenstion }
- end
+ describe '#archive_metadata' do
+ let(:storage_path) { '/tmp' }
+ let(:cache_key) { File.join(repository.gl_repository, SeedRepo::LastCommit::ID) }
- describe '#archive_prefix' do
- let(:project_name) { 'project-name'}
+ let(:append_sha) { true }
+ let(:ref) { 'master' }
+ let(:format) { nil }
- before do
- expect(repository).to receive(:name).once.and_return(project_name)
- end
+ let(:expected_extension) { 'tar.gz' }
+ let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
+ let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
+ let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
- it 'returns parameterised string for a ref containing slashes' do
- prefix = repository.archive_prefix('test/branch', 'SHA', append_sha: nil)
+ subject(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: append_sha) }
- expect(prefix).to eq("#{project_name}-test-branch-SHA")
+ it 'sets CommitId to the commit SHA' do
+ expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
end
- it 'returns correct string for a ref containing dots' do
- prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: nil)
-
- expect(prefix).to eq("#{project_name}-test.branch-SHA")
+ it 'sets ArchivePrefix to the expected prefix' do
+ expect(metadata['ArchivePrefix']).to eq(expected_prefix)
end
- it 'returns string with sha when append_sha is false' do
- prefix = repository.archive_prefix('test.branch', 'SHA', append_sha: false)
+ it 'sets ArchivePath to the expected globally-unique path' do
+ # This is really important from a security perspective. Think carefully
+ # before changing it: https://gitlab.com/gitlab-org/gitlab-ce/issues/45689
+ expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
- expect(prefix).to eq("#{project_name}-test.branch")
+ expect(metadata['ArchivePath']).to eq(expected_path)
end
- end
-
- describe '#archive' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', append_sha: true) }
- it_should_behave_like 'archive check', '.tar.gz'
- end
-
- describe '#archive_zip' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip', append_sha: true) }
-
- it_should_behave_like 'archive check', '.zip'
- end
+ context 'append_sha varies archive path and filename' do
+ where(:append_sha, :ref, :expected_prefix) do
+ sha = SeedRepo::LastCommit::ID
- describe '#archive_bz2' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2', append_sha: true) }
+ true | 'master' | "gitlab-git-test-master-#{sha}"
+ true | sha | "gitlab-git-test-#{sha}-#{sha}"
+ false | 'master' | "gitlab-git-test-master"
+ false | sha | "gitlab-git-test-#{sha}"
+ nil | 'master' | "gitlab-git-test-master-#{sha}"
+ nil | sha | "gitlab-git-test-#{sha}"
+ end
- it_should_behave_like 'archive check', '.tar.bz2'
- end
+ with_them do
+ it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) }
+ it { expect(metadata['ArchivePath']).to eq(expected_path) }
+ end
+ end
- describe '#archive_fallback' do
- let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup', append_sha: true) }
+ context 'format varies archive path and filename' do
+ where(:format, :expected_extension) do
+ nil | 'tar.gz'
+ 'madeup' | 'tar.gz'
+ 'tbz2' | 'tar.bz2'
+ 'zip' | 'zip'
+ end
- it_should_behave_like 'archive check', '.tar.gz'
+ with_them do
+ it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) }
+ it { expect(metadata['ArchivePath']).to eq(expected_path) }
+ end
+ end
end
describe '#size' do
@@ -587,6 +596,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ it 'does not fail when deleting an empty list of refs' do
+ expect { repo.delete_refs(*[]) }.not_to raise_error
+ end
+
it 'raises an error if it failed' do
expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError)
end
@@ -602,32 +615,22 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#branch_names_contains_sha' do
- shared_examples 'returning the right branches' do
- let(:head_id) { repository.rugged.head.target.oid }
- let(:new_branch) { head_id }
- let(:utf8_branch) { 'branch-é' }
-
- before do
- repository.create_branch(new_branch, 'master')
- repository.create_branch(utf8_branch, 'master')
- end
-
- after do
- repository.delete_branch(new_branch)
- repository.delete_branch(utf8_branch)
- end
+ let(:head_id) { repository.rugged.head.target.oid }
+ let(:new_branch) { head_id }
+ let(:utf8_branch) { 'branch-é' }
- it 'displays that branch' do
- expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch)
- end
+ before do
+ repository.create_branch(new_branch, 'master')
+ repository.create_branch(utf8_branch, 'master')
end
- context 'when Gitaly is enabled' do
- it_behaves_like 'returning the right branches'
+ after do
+ repository.delete_branch(new_branch)
+ repository.delete_branch(utf8_branch)
end
- context 'when Gitaly is disabled', :disable_gitaly do
- it_behaves_like 'returning the right branches'
+ it 'displays that branch' do
+ expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch)
end
end
@@ -689,7 +692,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
after do
- Gitlab::Shell.new.remove_repository(storage_path, 'my_project')
+ Gitlab::Shell.new.remove_repository('default', 'my_project')
end
shared_examples 'repository mirror fecthing' do
@@ -1055,41 +1058,51 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#raw_changes_between' do
- let(:old_rev) { }
- let(:new_rev) { }
- let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
+ shared_examples 'raw changes' do
+ let(:old_rev) { }
+ let(:new_rev) { }
+ let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
- context 'initial commit' do
- let(:old_rev) { Gitlab::Git::BLANK_SHA }
- let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
+ context 'initial commit' do
+ let(:old_rev) { Gitlab::Git::BLANK_SHA }
+ let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
- it 'returns the changes' do
- expect(changes).to be_present
- expect(changes.size).to eq(3)
+ it 'returns the changes' do
+ expect(changes).to be_present
+ expect(changes.size).to eq(3)
+ end
end
- end
- context 'with an invalid rev' do
- let(:old_rev) { 'foo' }
- let(:new_rev) { 'bar' }
+ context 'with an invalid rev' do
+ let(:old_rev) { 'foo' }
+ let(:new_rev) { 'bar' }
- it 'returns an error' do
- expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
+ it 'returns an error' do
+ expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
+ end
end
- end
- context 'with valid revs' do
- let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
- let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ context 'with valid revs' do
+ let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
+ let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
- it 'returns the changes' do
- expect(changes.size).to eq(9)
- expect(changes.first.operation).to eq(:modified)
- expect(changes.first.new_path).to eq('.gitmodules')
- expect(changes.last.operation).to eq(:added)
- expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ it 'returns the changes' do
+ expect(changes.size).to eq(9)
+ expect(changes.first.operation).to eq(:modified)
+ expect(changes.first.new_path).to eq('.gitmodules')
+ expect(changes.last.operation).to eq(:added)
+ expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
+ end
end
end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like 'raw changes'
+ end
+
+ context 'when gitaly is disabled', :disable_gitaly do
+ it_behaves_like 'raw changes'
+ end
end
describe '#merge_base' do
@@ -2234,51 +2247,42 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#checksum' do
- shared_examples 'calculating checksum' do
- it 'calculates the checksum for non-empty repo' do
- expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
- end
-
- it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
- FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
+ it 'calculates the checksum for non-empty repo' do
+ expect(repository.checksum).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
+ end
- system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
- chdir: storage_path,
- out: '/dev/null',
- err: '/dev/null')
+ it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
+ FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
- empty_repo = described_class.new('default', 'empty-repo.git', '')
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
+ chdir: storage_path,
+ out: '/dev/null',
+ err: '/dev/null')
- expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
- end
+ empty_repo = described_class.new('default', 'empty-repo.git', '')
- it 'raises a no repository exception when there is no repo' do
- broken_repo = described_class.new('default', 'a/path.git', '')
-
- expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
end
- context 'when calculate_checksum Gitaly feature is enabled' do
- it_behaves_like 'calculating checksum'
- end
+ it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
+ FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
- context 'when calculate_checksum Gitaly feature is disabled', :disable_gitaly do
- it_behaves_like 'calculating checksum'
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
- describe 'when storage is broken', :broken_storage do
- it 'raises a storage exception when storage is not available' do
- broken_repo = described_class.new('broken', 'a/path.git', '')
+ File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
- expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible)
- end
- end
+ non_valid = described_class.new('default', 'non-valid.git', '')
+
+ expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
+ end
- it "raises a Gitlab::Git::Repository::Failure error if the `popen` call to git returns a non-zero exit code" do
- allow(repository).to receive(:popen).and_return(['output', nil])
+ it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
+ broken_repo = described_class.new('default', 'a/path.git', '')
- expect { repository.checksum }.to raise_error Gitlab::Git::Repository::ChecksumError
- end
+ expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index 6c4f538bf01..be2f5bfb819 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -32,4 +32,56 @@ describe Gitlab::Git::Tag, seed_helper: true do
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] }
+
+ subject 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
+ end
+
+ context 'when Gitaly tag_messages feature is disabled', :disable_gitaly do
+ it_behaves_like 'getting tag messages'
+ end
+ end
+
+ describe 'tag into from Gitaly tag' do
+ context 'message_size != message.size' do
+ let(:gitaly_tag) { build(:gitaly_tag, message: ''.b, message_size: message_size) }
+ let(:tag) { described_class.new(repository, gitaly_tag) }
+
+ context 'message_size less than threshold' do
+ let(:message_size) { 123 }
+
+ it 'fetches tag message seperately' do
+ expect(described_class).to receive(:get_message).with(repository, gitaly_tag.id)
+
+ tag.message
+ end
+ end
+
+ context 'message_size greater than threshold' do
+ let(:message_size) { described_class::MAX_TAG_MESSAGE_DISPLAY_SIZE + 1 }
+
+ it 'returns a notice about message size' do
+ expect(tag.message).to eq("--tag message is too big")
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index 761f7732036..722d697c28e 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Git::Wiki do
end
def commit_details(name)
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "created page #{name}")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "created page #{name}")
end
def destroy_page(title, dir = '')
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 6c625596605..dfffea7797f 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe Gitlab::GitAccess do
- set(:user) { create(:user) }
+ include TermsHelper
+
+ let(:user) { create(:user) }
let(:actor) { user }
let(:project) { create(:project, :repository) }
@@ -1040,6 +1042,96 @@ describe Gitlab::GitAccess do
end
end
+ context 'terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ shared_examples 'access after accepting terms' do
+ let(:actions) do
+ [-> { pull_access_check },
+ -> { push_access_check }]
+ end
+
+ it 'blocks access when the user did not accept terms', :aggregate_failures do
+ actions.each do |action|
+ expect { action.call }.to raise_unauthorized(/must accept the Terms of Service in order to perform this action/)
+ end
+ end
+
+ it 'allows access when the user accepted the terms', :aggregate_failures do
+ accept_terms(user)
+
+ actions.each do |action|
+ expect { action.call }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'as an anonymous user to a public project' do
+ let(:actor) { nil }
+ let(:project) { create(:project, :public, :repository) }
+
+ it { expect { pull_access_check }.not_to raise_error }
+ end
+
+ describe 'as a guest to a public project' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'access after accepting terms' do
+ let(:actions) { [-> { pull_access_check }] }
+ end
+ end
+
+ describe 'as a reporter to the project' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'access after accepting terms' do
+ let(:actions) { [-> { pull_access_check }] }
+ end
+ end
+
+ describe 'as a developer of the project' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'as a master of the project' do
+ before do
+ project.add_master(user)
+ end
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'as an owner of the project' do
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ it_behaves_like 'access after accepting terms'
+ end
+
+ describe 'when a ci build clones the project' do
+ let(:protocol) { 'http' }
+ let(:authentication_abilities) { [:build_download_code] }
+ let(:auth_result_type) { :build }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it "doesn't block http pull" do
+ aggregate_failures do
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+ end
+
private
def raise_unauthorized(message)
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 074323d47d2..1547d447197 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -156,4 +156,26 @@ describe Gitlab::GitalyClient::RepositoryService do
client.calculate_checksum
end
end
+
+ describe '#create_from_snapshot' do
+ it 'sends a create_repository_from_snapshot message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:create_repository_from_snapshot)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double)
+
+ client.create_from_snapshot('http://example.com?wiki=1', 'Custom xyz')
+ end
+ end
+
+ describe '#raw_changes_between' do
+ it 'sends a create_repository_from_snapshot message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:get_raw_changes)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double)
+
+ client.raw_changes_between('deadbeef', 'deadpork')
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
new file mode 100644
index 00000000000..c89913ec8e9
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::StorageSettings do
+ describe "#initialize" do
+ context 'when the storage contains no path' do
+ it 'raises an error' do
+ expect do
+ described_class.new("foo" => {})
+ end.to raise_error(described_class::InvalidConfigurationError)
+ end
+ end
+
+ context "when the argument isn't a hash" do
+ it 'raises an error' do
+ expect do
+ described_class.new("test")
+ end.to raise_error("expected a Hash, got a String")
+ end
+ end
+
+ context 'when the storage is valid' do
+ it 'raises no error' do
+ expect do
+ described_class.new("path" => Rails.root)
+ end.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 879b1d9fb0f..cc9e4b67e72 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::GithubImport::Importer::RepositoryImporter do
let(:repository) { double(:repository) }
+ let(:import_state) { double(:import_state) }
let(:client) { double(:client) }
let(:project) do
@@ -12,7 +13,8 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
repository_storage: 'foo',
disk_path: 'foo',
repository: repository,
- create_wiki: true
+ create_wiki: true,
+ import_state: import_state
)
end
diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
index e2a821d4d5c..20b48c1de68 100644
--- a/spec/lib/gitlab/github_import/parallel_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
@@ -12,6 +12,8 @@ describe Gitlab::GithubImport::ParallelImporter do
let(:importer) { described_class.new(project) }
before do
+ create(:import_state, :started, project: project)
+
expect(Gitlab::GithubImport::Stage::ImportRepositoryWorker)
.to receive(:perform_async)
.with(project.id)
@@ -34,7 +36,7 @@ describe Gitlab::GithubImport::ParallelImporter do
it 'updates the import JID of the project' do
importer.execute
- expect(project.import_jid).to eq("github-importer/#{project.id}")
+ expect(project.reload.import_jid).to eq("github-importer/#{project.id}")
end
end
end
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 82548c7fd31..5ea086e4abd 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::GitlabImport::ProjectCreator do
owner: { name: "john" }
}.with_indifferent_access
end
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
let(:token) { "asdffg" }
let(:access_params) { { gitlab_access_token: token } }
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
index 8d5b60d50de..24cd518c77b 100644
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do
"repositoryUrls" => ["https://vim.googlecode.com/git/"]
)
end
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
before do
namespace.add_owner(user)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 897a5984782..fb5fd300dbb 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -35,6 +35,7 @@ notes:
- todos
- events
- system_note_metadata
+- note_diff_file
label_links:
- target
- label
@@ -185,6 +186,7 @@ project:
- cluster
- clusters
- cluster_project
+- cluster_ingresses
- creator
- group
- namespace
@@ -258,7 +260,6 @@ project:
- builds
- runner_projects
- runners
-- active_runners
- variables
- triggers
- pipeline_schedules
@@ -269,13 +270,16 @@ project:
- pages_domains
- authorized_users
- project_authorizations
+- remote_mirrors
- route
- redirect_routes
- statistics
- container_repositories
- uploads
+- import_state
- members_and_requesters
- build_trace_section_names
+- build_trace_chunks
- root_of_fork_network
- fork_network_member
- fork_network
@@ -286,6 +290,8 @@ project:
- internal_ids
- project_deploy_tokens
- deploy_tokens
+- settings
+- ci_cd_settings
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 6d63749296e..4f64f2bd6b4 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6760,26 +6760,6 @@
"wiki_page_events": true
},
{
- "id": 92,
- "title": "Gemnasium",
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.202Z",
- "updated_at": "2016-06-14T15:01:51.202Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "type": "GemnasiumService",
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
"id": 91,
"title": "Flowdock",
"project_id": 5,
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index f84a777a27f..62da967cf96 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -232,6 +232,7 @@ Ci::Stage:
- id
- name
- status
+- position
- lock_version
- project_id
- pipeline_id
@@ -537,12 +538,6 @@ ProjectCustomAttribute:
- project_id
- key
- value
-LfsFileLock:
-- id
-- path
-- user_id
-- project_id
-- created_at
Badge:
- id
- link_url
@@ -552,3 +547,5 @@ Badge:
- created_at
- updated_at
- type
+ProjectCiCdSetting:
+- group_runners_enabled
diff --git a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
index d2bd8ccdf3f..24bc231d5a0 100644
--- a/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_repo_saver_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::WikiRepoSaver do
describe 'bundle a wiki Git repo' do
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, name: 'searchable_project') }
+ let!(:project) { create(:project, :public, :wiki_repo, name: 'searchable_project') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
diff --git a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
index 5c01ee0ebb8..f99f198da33 100644
--- a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb
@@ -24,8 +24,8 @@ describe Gitlab::ImportExport::WikiRestorer do
after do
FileUtils.rm_rf(export_path)
- Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage_path, project_with_wiki.wiki.disk_path)
- Gitlab::Shell.new.remove_repository(project.wiki.repository_storage_path, project.wiki.disk_path)
+ Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage, project_with_wiki.wiki.disk_path)
+ Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path)
end
it 'restores the wiki repo successfully' do
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index c959add7a36..4c0c3fcbcc7 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::IncomingEmail do
end
describe 'self.supports_wildcard?' do
- context 'address contains the wildard placeholder' do
+ context 'address contains the wildcard placeholder' do
before do
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
end
@@ -49,7 +49,7 @@ describe Gitlab::IncomingEmail do
stub_incoming_email_setting(address: nil)
end
- it 'returns that wildard is not supported' do
+ it 'returns that wildcard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
end
end
@@ -83,6 +83,10 @@ describe Gitlab::IncomingEmail do
it "returns reply key" do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end
+
+ it 'does not match emails with extra bits' do
+ expect(described_class.key_from_address('somereplies+somekey@example.com.someotherdomain.com')).to be nil
+ end
end
context 'self.key_from_fallback_message_id' do
diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
index 3cfdae794f6..7be8be54d5e 100644
--- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
@@ -4,22 +4,10 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
let(:application) { create(:clusters_applications_helm) }
let(:base_command) { described_class.new(application.name) }
- describe '#generate_script' do
- let(:helm_version) { Gitlab::Kubernetes::Helm::HELM_VERSION }
- let(:command) do
- <<~HEREDOC
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{helm_version}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- HEREDOC
- end
-
- subject { base_command.generate_script }
+ subject { base_command }
- it 'should return a command that prepares the environment for helm-cli' do
- expect(subject).to eq(command)
- end
+ it_behaves_like 'helm commands' do
+ let(:commands) { '' }
end
describe '#pod_resource' do
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
index e6920b0a76f..89e36a298f8 100644
--- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -2,23 +2,9 @@ require 'spec_helper'
describe Gitlab::Kubernetes::Helm::InitCommand do
let(:application) { create(:clusters_applications_helm) }
- let(:init_command) { described_class.new(application.name) }
+ let(:commands) { 'helm init >/dev/null' }
- describe '#generate_script' do
- let(:command) do
- <<~MSG.chomp
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- helm init >/dev/null
- MSG
- end
+ subject { described_class.new(application.name) }
- subject { init_command.generate_script }
-
- it 'should return the appropriate command' do
- is_expected.to eq(command)
- end
- end
+ it_behaves_like 'helm commands'
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index 137b8f718de..547f3f1752c 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -12,50 +12,36 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
)
end
- describe '#generate_script' do
- let(:command) do
- <<~MSG
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- helm init --client-only >/dev/null
- helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
- MSG
- end
-
- subject { install_command.generate_script }
+ subject { install_command }
- it 'should return appropriate command' do
- is_expected.to eq(command)
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ EOS
end
+ end
- context 'with an application with a repository' do
- let(:ci_runner) { create(:ci_runner) }
- let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
- let(:install_command) do
- described_class.new(
- application.name,
- chart: application.chart,
- values: application.values,
- repository: application.repository
- )
- end
-
- let(:command) do
- <<~MSG
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
- helm init --client-only >/dev/null
- helm repo add #{application.name} #{application.repository}
- helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
- MSG
- end
+ context 'with an application with a repository' do
+ let(:ci_runner) { create(:ci_runner) }
+ let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
+ let(:install_command) do
+ described_class.new(
+ application.name,
+ chart: application.chart,
+ values: application.values,
+ repository: application.repository
+ )
+ end
- it 'should return appropriate command' do
- is_expected.to eq(command)
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm repo add #{application.name} #{application.repository}
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ EOS
end
end
end
diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
index 737c9a624e0..eb1b13704ea 100644
--- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::LegacyGithubImport::ProjectCreator do
let(:user) { create(:user) }
- let(:namespace) { create(:group, owner: user) }
+ let(:namespace) { create(:group) }
let(:repo) do
OpenStruct.new(
diff --git a/spec/lib/gitlab/metrics/prometheus_spec.rb b/spec/lib/gitlab/metrics/prometheus_spec.rb
new file mode 100644
index 00000000000..3d4dd5fdf01
--- /dev/null
+++ b/spec/lib/gitlab/metrics/prometheus_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Prometheus, :prometheus do
+ let(:all_metrics) { Gitlab::Metrics }
+ let(:registry) { all_metrics.registry }
+
+ describe '#reset_registry!' do
+ it 'clears existing metrics' do
+ registry.counter(:test, 'test metric')
+
+ expect(registry.metrics.count).to eq(1)
+
+ all_metrics.reset_registry!
+
+ expect(all_metrics.registry.metrics.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 1d162f53a13..6eb0600f49e 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -180,11 +180,11 @@ describe Gitlab::Metrics::WebTransaction do
end
context 'when request goes to ActionController' do
- let(:content_type) { 'text/html' }
+ let(:request) { double(:request, format: double(:format, ref: :html)) }
before do
klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
+ controller = double(:controller, class: klass, action_name: 'show', request: request)
env['action_controller.instance'] = controller
end
@@ -195,7 +195,7 @@ describe Gitlab::Metrics::WebTransaction do
end
context 'when the response content type is not :html' do
- let(:content_type) { 'application/json' }
+ let(:request) { double(:request, format: double(:format, ref: :json)) }
it 'appends the mime type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
diff --git a/spec/lib/gitlab/pages_client_spec.rb b/spec/lib/gitlab/pages_client_spec.rb
new file mode 100644
index 00000000000..da6d26f4aee
--- /dev/null
+++ b/spec/lib/gitlab/pages_client_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe Gitlab::PagesClient do
+ subject { described_class }
+
+ describe '.token' do
+ it 'returns the token as it is on disk' do
+ pending 'add omnibus support for generating the secret file https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466'
+ expect(subject.token).to eq(File.read('.gitlab_pages_secret'))
+ end
+ end
+
+ describe '.read_or_create_token' do
+ subject { described_class.read_or_create_token }
+ let(:token_path) { 'tmp/tests/gitlab-pages-secret' }
+ before do
+ allow(described_class).to receive(:token_path).and_return(token_path)
+ FileUtils.rm_f(token_path)
+ end
+
+ it 'uses the existing token file if it exists' do
+ secret = 'existing secret'
+ File.write(token_path, secret)
+
+ subject
+ expect(described_class.token).to eq(secret)
+ end
+
+ it 'creates one if none exists' do
+ pending 'add omnibus support for generating the secret file https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2466'
+
+ old_token = described_class.token
+ # sanity check
+ expect(File.exist?(token_path)).to eq(false)
+
+ subject
+ expect(described_class.token.bytesize).to eq(64)
+ expect(described_class.token).not_to eq(old_token)
+ end
+ end
+
+ describe '.write_token' do
+ let(:token_path) { 'tmp/tests/gitlab-pages-secret' }
+ before do
+ allow(described_class).to receive(:token_path).and_return(token_path)
+ FileUtils.rm_f(token_path)
+ end
+
+ it 'writes the secret' do
+ new_secret = 'hello new secret'
+ expect(File.exist?(token_path)).to eq(false)
+
+ described_class.send(:write_token, new_secret)
+
+ expect(File.read(token_path)).to eq(new_secret)
+ end
+
+ it 'does nothing if the file already exists' do
+ existing_secret = 'hello secret'
+ File.write(token_path, existing_secret)
+
+ described_class.send(:write_token, 'new secret')
+
+ expect(File.read(token_path)).to eq(existing_secret)
+ end
+ end
+
+ describe '.load_certificate' do
+ subject { described_class.load_certificate }
+ before do
+ allow(described_class).to receive(:config).and_return(config)
+ end
+
+ context 'with no certificate in the config' do
+ let(:config) { double(:config, certificate: '') }
+
+ it 'does not set @certificate' do
+ subject
+
+ expect(described_class.certificate).to be_nil
+ end
+ end
+
+ context 'with a certificate path in the config' do
+ let(:certificate_path) { 'tmp/tests/fake-certificate' }
+ let(:config) { double(:config, certificate: certificate_path) }
+
+ it 'sets @certificate' do
+ certificate_data = "--- BEGIN CERTIFICATE ---\nbla\n--- END CERTIFICATE ---\n"
+ File.write(certificate_path, certificate_data)
+ subject
+
+ expect(described_class.certificate).to eq(certificate_data)
+ end
+ end
+ end
+
+ describe '.request_kwargs' do
+ let(:token) { 'secret token' }
+ let(:auth_header) { 'Bearer c2VjcmV0IHRva2Vu' }
+ before do
+ allow(described_class).to receive(:token).and_return(token)
+ end
+
+ context 'without timeout' do
+ it { expect(subject.send(:request_kwargs, nil)[:metadata]['authorization']).to eq(auth_header) }
+ end
+
+ context 'with timeout' do
+ let(:timeout) { 1.second }
+
+ it 'still sets the authorization header' do
+ expect(subject.send(:request_kwargs, timeout)[:metadata]['authorization']).to eq(auth_header)
+ end
+
+ it 'sets a deadline value' do
+ now = Time.now
+ deadline = subject.send(:request_kwargs, timeout)[:deadline]
+
+ expect(deadline).to be_between(now, now + 2 * timeout)
+ end
+ end
+ end
+
+ describe '.stub' do
+ before do
+ allow(described_class).to receive(:address).and_return('unix:/foo/bar')
+ end
+
+ it { expect(subject.send(:stub, :health_check)).to be_a(Grpc::Health::V1::Health::Stub) }
+ end
+
+ describe '.address' do
+ subject { described_class.send(:address) }
+
+ before do
+ allow(described_class).to receive(:config).and_return(config)
+ end
+
+ context 'with a unix: address' do
+ let(:config) { double(:config, address: 'unix:/foo/bar') }
+
+ it { expect(subject).to eq('unix:/foo/bar') }
+ end
+
+ context 'with a tcp:// address' do
+ let(:config) { double(:config, address: 'tcp://localhost:1234') }
+
+ it { expect(subject).to eq('localhost:1234') }
+ end
+ end
+
+ describe '.grpc_creds' do
+ subject { described_class.send(:grpc_creds) }
+
+ before do
+ allow(described_class).to receive(:config).and_return(config)
+ end
+
+ context 'with a unix: address' do
+ let(:config) { double(:config, address: 'unix:/foo/bar') }
+
+ it { expect(subject).to eq(:this_channel_is_insecure) }
+ end
+
+ context 'with a tcp:// address' do
+ let(:config) { double(:config, address: 'tcp://localhost:1234') }
+
+ it { expect(subject).to be_a(GRPC::Core::ChannelCredentials) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 8351b967133..e3f705d2299 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -106,6 +106,18 @@ describe Gitlab::ProjectSearchResults do
end
end
+ context 'when the matching content contains multiple null bytes' do
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" }
+
+ it 'returns a valid FoundBlob' do
+ expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.basename).to eq('testdata/foo')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("blah\x001\x00foo")
+ end
+ end
+
context 'when the search result ends with an empty line' do
let(:results) { project.repository.search_files_by_content('Role models', 'master') }
@@ -175,14 +187,14 @@ describe Gitlab::ProjectSearchResults do
end
describe 'wiki search' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :wiki_repo) }
let(:wiki) { build(:project_wiki, project: project) }
let!(:wiki_page) { wiki.create_page('Title', 'Content') }
subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
context 'when wiki is disabled' do
- let(:project) { create(:project, :public, :wiki_disabled) }
+ let(:project) { create(:project, :public, :wiki_repo, :wiki_disabled) }
it 'hides wiki blobs from members' do
project.add_reporter(user)
@@ -196,7 +208,7 @@ describe Gitlab::ProjectSearchResults do
end
context 'when wiki is internal' do
- let(:project) { create(:project, :public, :wiki_private) }
+ let(:project) { create(:project, :public, :wiki_repo, :wiki_private) }
it 'finds wiki blobs for guest' do
project.add_guest(user)
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index f030f371372..13940713dfc 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -45,25 +45,6 @@ describe ::Gitlab::RepoPath do
end
end
- describe '.strip_storage_path' do
- before do
- allow(Gitlab.config.repositories).to receive(:storages).and_return({
- 'storage1' => Gitlab::GitalyClient::StorageSettings.new('path' => '/foo'),
- 'storage2' => Gitlab::GitalyClient::StorageSettings.new('path' => '/bar')
- })
- end
-
- it 'strips the storage path' do
- expect(described_class.strip_storage_path('/bar/foo/qux/baz.git')).to eq('foo/qux/baz.git')
- end
-
- it 'raises NotFoundError if no storage matches the path' do
- expect { described_class.strip_storage_path('/doesnotexist/foo.git') }.to raise_error(
- described_class::NotFoundError
- )
- end
- end
-
describe '.find_project' do
let(:project) { create(:project) }
let(:redirect) { project.route.create_redirect('foo/bar/baz') }
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 7f579df1c36..bf6ee4b0b59 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -447,18 +447,18 @@ describe Gitlab::Shell do
let(:disk_path) { "#{project.disk_path}.git" }
it 'returns true when the command succeeds' do
- expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(true)
- expect(gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path)).to be(true)
+ expect(gitlab_shell.remove_repository(project.repository_storage, project.disk_path)).to be(true)
- expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false)
+ expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(false)
end
it 'keeps the namespace directory' do
- gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path)
+ gitlab_shell.remove_repository(project.repository_storage, project.disk_path)
- expect(gitlab_shell.exists?(project.repository_storage_path, disk_path)).to be(false)
- expect(gitlab_shell.exists?(project.repository_storage_path, project.disk_path.gsub(project.name, ''))).to be(true)
+ expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(false)
+ expect(gitlab_shell.exists?(project.repository_storage, project.disk_path.gsub(project.name, ''))).to be(true)
end
end
@@ -469,18 +469,18 @@ describe Gitlab::Shell do
old_path = project2.disk_path
new_path = "project/new_path"
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(true)
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(false)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{old_path}.git")).to be(true)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{new_path}.git")).to be(false)
- expect(gitlab_shell.mv_repository(project2.repository_storage_path, old_path, new_path)).to be_truthy
+ expect(gitlab_shell.mv_repository(project2.repository_storage, old_path, new_path)).to be_truthy
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{old_path}.git")).to be(false)
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{new_path}.git")).to be(true)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{old_path}.git")).to be(false)
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{new_path}.git")).to be(true)
end
it 'returns false when the command fails' do
- expect(gitlab_shell.mv_repository(project2.repository_storage_path, project2.disk_path, '')).to be_falsy
- expect(gitlab_shell.exists?(project2.repository_storage_path, "#{project2.disk_path}.git")).to be(true)
+ expect(gitlab_shell.mv_repository(project2.repository_storage, project2.disk_path, '')).to be_falsy
+ expect(gitlab_shell.exists?(project2.repository_storage, "#{project2.disk_path}.git")).to be(true)
end
end
@@ -679,48 +679,48 @@ describe Gitlab::Shell do
describe 'namespace actions' do
subject { described_class.new }
- let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
+ let(:storage) { Gitlab.config.repositories.storages.keys.first }
describe '#add_namespace' do
it 'creates a namespace' do
- subject.add_namespace(storage_path, "mepmep")
+ subject.add_namespace(storage, "mepmep")
- expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ expect(subject.exists?(storage, "mepmep")).to be(true)
end
end
describe '#exists?' do
context 'when the namespace does not exist' do
it 'returns false' do
- expect(subject.exists?(storage_path, "non-existing")).to be(false)
+ expect(subject.exists?(storage, "non-existing")).to be(false)
end
end
context 'when the namespace exists' do
it 'returns true' do
- subject.add_namespace(storage_path, "mepmep")
+ subject.add_namespace(storage, "mepmep")
- expect(subject.exists?(storage_path, "mepmep")).to be(true)
+ expect(subject.exists?(storage, "mepmep")).to be(true)
end
end
end
describe '#remove' do
it 'removes the namespace' do
- subject.add_namespace(storage_path, "mepmep")
- subject.rm_namespace(storage_path, "mepmep")
+ subject.add_namespace(storage, "mepmep")
+ subject.rm_namespace(storage, "mepmep")
- expect(subject.exists?(storage_path, "mepmep")).to be(false)
+ expect(subject.exists?(storage, "mepmep")).to be(false)
end
end
describe '#mv_namespace' do
it 'renames the namespace' do
- subject.add_namespace(storage_path, "mepmep")
- subject.mv_namespace(storage_path, "mepmep", "2mep")
+ subject.add_namespace(storage, "mepmep")
+ subject.mv_namespace(storage, "mepmep", "2mep")
- expect(subject.exists?(storage_path, "mepmep")).to be(false)
- expect(subject.exists?(storage_path, "2mep")).to be(true)
+ expect(subject.exists?(storage, "mepmep")).to be(false)
+ expect(subject.exists?(storage, "2mep")).to be(true)
end
end
end
diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb
index bed58d407ef..0a6ac0aa294 100644
--- a/spec/lib/gitlab/untrusted_regexp_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp_spec.rb
@@ -1,6 +1,49 @@
-require 'spec_helper'
+require 'fast_spec_helper'
+require 'support/shared_examples/malicious_regexp_shared_examples'
describe Gitlab::UntrustedRegexp do
+ describe '.valid?' do
+ it 'returns true if regexp is valid' do
+ expect(described_class.valid?('/some ( thing/'))
+ .to be false
+ end
+
+ it 'returns true if regexp is invalid' do
+ expect(described_class.valid?('/some .* thing/'))
+ .to be true
+ end
+ end
+
+ describe '.fabricate' do
+ context 'when regexp is using /regexp/ scheme with flags' do
+ it 'fabricates regexp with a single flag' do
+ regexp = described_class.fabricate('/something/i')
+
+ expect(regexp).to eq described_class.new('(?i)something')
+ expect(regexp.scan('SOMETHING')).to be_one
+ end
+
+ it 'fabricates regexp with multiple flags' do
+ regexp = described_class.fabricate('/something/im')
+
+ expect(regexp).to eq described_class.new('(?im)something')
+ end
+
+ it 'fabricates regexp without flags' do
+ regexp = described_class.fabricate('/something/')
+
+ expect(regexp).to eq described_class.new('something')
+ end
+ end
+
+ context 'when regexp is a raw pattern' do
+ it 'raises an error' do
+ expect { described_class.fabricate('some .* thing') }
+ .to raise_error(RegexpError)
+ end
+ end
+ end
+
describe '#initialize' do
subject { described_class.new(pattern) }
@@ -39,6 +82,14 @@ describe Gitlab::UntrustedRegexp do
expect(result).to be_falsy
end
+
+ it 'can handle regular expressions in multiline mode' do
+ regexp = described_class.new('^\d', multiline: true)
+
+ result = regexp === "Header\n\n1. Content"
+
+ expect(result).to be_truthy
+ end
end
describe '#scan' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 9e6aa109a4b..a716e6f5434 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -96,6 +96,7 @@ describe Gitlab::UsageData do
pages_domains
protected_branches
releases
+ remote_mirrors
snippets
todos
uploads
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 40c8286b1b9..97b6069f64d 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -32,6 +32,12 @@ describe Gitlab::UserAccess do
let(:empty_project) { create(:project_empty_repo) }
let(:project_access) { described_class.new(user, project: empty_project) }
+ it 'returns true for admins' do
+ user.update!(admin: true)
+
+ expect(access.can_push_to_branch?('master')).to be_truthy
+ end
+
it 'returns true if user is master' do
empty_project.add_master(user)
@@ -71,6 +77,12 @@ describe Gitlab::UserAccess do
let(:branch) { create :protected_branch, project: project, name: "test" }
let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
+ it 'returns true for admins' do
+ user.update!(admin: true)
+
+ expect(access.can_push_to_branch?(branch.name)).to be_truthy
+ end
+
it 'returns true if user is a master' do
project.add_master(user)
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
index 32a946ca034..4eca53032a2 100644
--- a/spec/lib/gitlab/view/presenter/base_spec.rb
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -48,4 +48,11 @@ describe Gitlab::View::Presenter::Base do
end
end
end
+
+ describe '#present' do
+ it 'returns self' do
+ presenter = presenter_class.new(build_stubbed(:project))
+ expect(presenter.present).to eq(presenter)
+ end
+ end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d64ea72e346..660671cefaf 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Workhorse do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
let(:repository) { project.repository }
def decode_workhorse_header(array)
@@ -55,16 +55,6 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
-
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to eq(base_params)
- end
- end
-
context "when the repository doesn't have an archive file path" do
before do
allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
@@ -482,4 +472,26 @@ describe Gitlab::Workhorse do
}.deep_stringify_keys)
end
end
+
+ describe '.send_git_snapshot' do
+ let(:url) { 'http://example.com' }
+
+ subject(:request) { described_class.send_git_snapshot(repository) }
+
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(request)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq('git-snapshot')
+ expect(params).to eq(
+ 'GitalyServer' => {
+ 'address' => Gitlab::GitalyClient.address(project.repository_storage),
+ 'token' => Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
+ repository: repository.gitaly_repository
+ ).to_json
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index bd443a5d9e7..d63f448883b 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -1,6 +1,74 @@
-require 'rails_helper'
+require 'fast_spec_helper'
+
+require_dependency 'gitlab'
describe Gitlab do
+ describe '.root' do
+ it 'returns the root path of the app' do
+ expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__)))
+ end
+ end
+ describe '.revision' do
+ let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] }
+
+ around do |example|
+ described_class.instance_variable_set(:@_revision, nil)
+ example.run
+ described_class.instance_variable_set(:@_revision, nil)
+ end
+
+ context 'when a REVISION file exists' do
+ before do
+ expect(File).to receive(:exist?)
+ .with(described_class.root.join('REVISION'))
+ .and_return(true)
+ end
+
+ it 'returns the actual Git revision' do
+ expect(File).to receive(:read)
+ .with(described_class.root.join('REVISION'))
+ .and_return("abc123\n")
+
+ expect(described_class.revision).to eq('abc123')
+ end
+
+ it 'memoizes the revision' do
+ expect(File).to receive(:read)
+ .once
+ .with(described_class.root.join('REVISION'))
+ .and_return("abc123\n")
+
+ 2.times { described_class.revision }
+ end
+ end
+
+ context 'when no REVISION file exist' do
+ context 'when the Git command succeeds' do
+ before do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(cmd)
+ .and_return(Gitlab::Popen::Result.new(cmd, 'abc123', '', double(success?: true)))
+ end
+
+ it 'returns the actual Git revision' do
+ expect(described_class.revision).to eq('abc123')
+ end
+ end
+
+ context 'when the Git command fails' do
+ before do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with(cmd)
+ .and_return(Gitlab::Popen::Result.new(cmd, '', 'fatal: Not a git repository', double('Process::Status', success?: false)))
+ end
+
+ it 'returns "Unknown"' do
+ expect(described_class.revision).to eq('Unknown')
+ end
+ end
+ end
+ end
+
describe '.com?' do
it 'is true when on GitLab.com' do
stub_config_setting(url: 'https://gitlab.com')
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
new file mode 100644
index 00000000000..23485fbcb18
--- /dev/null
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe OmniAuth::Strategies::Jwt do
+ include Rack::Test::Methods
+ include DeviseHelpers
+
+ context '.decoded' do
+ let(:strategy) { described_class.new({}) }
+ let(:timestamp) { Time.now.to_i }
+ let(:jwt_config) { Devise.omniauth_configs[:jwt] }
+ let(:key) { JWT.encode(claims, jwt_config.strategy.secret) }
+
+ let(:claims) do
+ {
+ id: 123,
+ name: "user_example",
+ email: "user@example.com",
+ iat: timestamp
+ }
+ end
+
+ before do
+ allow_any_instance_of(OmniAuth::Strategy).to receive(:options).and_return(jwt_config.strategy)
+ allow_any_instance_of(Rack::Request).to receive(:params).and_return({ 'jwt' => key })
+ end
+
+ it 'decodes the user information' do
+ result = strategy.decoded
+
+ expect(result["id"]).to eq(123)
+ expect(result["name"]).to eq("user_example")
+ expect(result["email"]).to eq("user@example.com")
+ expect(result["iat"]).to eq(timestamp)
+ end
+
+ context 'required claims is missing' do
+ let(:claims) do
+ {
+ id: 123,
+ email: "user@example.com",
+ iat: timestamp
+ }
+ end
+
+ it 'raises error' do
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ end
+ end
+
+ context 'when valid_within is specified but iat attribute is missing in response' do
+ let(:claims) do
+ {
+ id: 123,
+ name: "user_example",
+ email: "user@example.com"
+ }
+ end
+
+ before do
+ jwt_config.strategy.valid_within = Time.now.to_i
+ end
+
+ it 'raises error' do
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ end
+ end
+
+ context 'when timestamp claim is too skewed from present' do
+ let(:claims) do
+ {
+ id: 123,
+ name: "user_example",
+ email: "user@example.com",
+ iat: timestamp - 10.minutes.to_i
+ }
+ end
+
+ before do
+ jwt_config.strategy.valid_within = 2.seconds
+ end
+
+ it 'raises error' do
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ end
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 43e419cd7de..f310a6854d5 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -390,6 +390,46 @@ describe Notify do
end
end
+ describe 'that are unmergeable' do
+ set(:merge_request) do
+ create(:merge_request, :conflict,
+ source_project: project,
+ target_project: project,
+ author: current_user,
+ assignee: assignee,
+ description: 'Awesome description')
+ end
+
+ subject { described_class.merge_request_unmergeable_email(recipient.id, merge_request.id) }
+
+ it_behaves_like 'a multiple recipients email'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { merge_request }
+ end
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'is sent as the merge request author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(merge_request.author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'has the correct subject and body' do
+ reasons = %w[foo bar]
+
+ allow_any_instance_of(MergeRequestPresenter).to receive(:unmergeable_reasons).and_return(reasons)
+ aggregate_failures do
+ is_expected.to have_referable_subject(merge_request, reply: true)
+ is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+ is_expected.to have_body_text('reasons:')
+ reasons.each do |reason|
+ is_expected.to have_body_text(reason)
+ end
+ end
+ end
+ end
+
shared_examples 'a push to an existing merge request' do
let(:push_user) { create(:user) }
@@ -594,7 +634,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.full_name} project"
is_expected.to have_html_escaped_body_text project.full_name
- is_expected.to have_body_text project.web_url
+ is_expected.to have_body_text project.full_name
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
end
@@ -654,38 +694,6 @@ describe Notify do
allow(Note).to receive(:find).with(note.id).and_return(note)
end
- shared_examples 'a note email' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it 'is sent to the given recipient as the author' do
- sender = subject.header[:from].addrs[0]
-
- aggregate_failures do
- expect(sender.display_name).to eq(note_author.name)
- expect(sender.address).to eq(gitlab_sender)
- expect(subject).to deliver_to(recipient.notification_email)
- end
- end
-
- it 'contains the message from the note' do
- is_expected.to have_html_escaped_body_text note.note
- end
-
- it 'does not contain note author' do
- is_expected.not_to have_body_text note.author_name
- end
-
- context 'when enabled email_author_in_body' do
- before do
- stub_application_setting(email_author_in_body: true)
- end
-
- it 'contains a link to note author' do
- is_expected.to have_html_escaped_body_text note.author_name
- end
- end
- end
-
describe 'on a commit' do
let(:commit) { project.commit }
diff --git a/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb b/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb
new file mode 100644
index 00000000000..6fd3cb1f44e
--- /dev/null
+++ b/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb')
+
+describe AddNotNullConstraintToProjectMirrorDataForeignKey, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:import_state) { table(:project_mirror_data) }
+
+ before do
+ import_state.create!(id: 1, project_id: nil, status: :started)
+ end
+
+ it 'removes every import state without an associated project_id' do
+ expect do
+ subject.up
+ end.to change { import_state.count }.from(1).to(0)
+ end
+end
diff --git a/spec/migrations/add_pipeline_build_foreign_key_spec.rb b/spec/migrations/add_pipeline_build_foreign_key_spec.rb
new file mode 100644
index 00000000000..e9413f52f19
--- /dev/null
+++ b/spec/migrations/add_pipeline_build_foreign_key_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180420010016_add_pipeline_build_foreign_key.rb')
+
+describe AddPipelineBuildForeignKey, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:builds) { table(:ci_builds) }
+
+ before do
+ namespaces.create(id: 10, name: 'gitlab-org', path: 'gitlab-org')
+ projects.create!(id: 11, namespace_id: 10, name: 'gitlab', path: 'gitlab')
+ pipelines.create!(id: 12, project_id: 11, ref: 'master', sha: 'adf43c3a')
+
+ builds.create!(id: 101, commit_id: 12, project_id: 11)
+ builds.create!(id: 102, commit_id: 222, project_id: 11)
+ builds.create!(id: 103, commit_id: 333, project_id: 11)
+ builds.create!(id: 104, commit_id: 12, project_id: 11)
+ builds.create!(id: 106, commit_id: nil, project_id: 11)
+ builds.create!(id: 107, commit_id: 12, project_id: nil)
+ end
+
+ it 'adds foreign key after removing orphans' do
+ expect(builds.all.count).to eq 6
+ expect(foreign_key_exists?(:ci_builds, :ci_pipelines, column: :commit_id)).to be_falsey
+
+ migrate!
+
+ expect(builds.all.pluck(:id)).to eq [101, 104]
+ expect(foreign_key_exists?(:ci_builds, :ci_pipelines, column: :commit_id)).to be_truthy
+ end
+end
diff --git a/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb
new file mode 100644
index 00000000000..bf299b70a29
--- /dev/null
+++ b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180511174224_add_unique_constraint_to_project_features_project_id.rb')
+
+describe AddUniqueConstraintToProjectFeaturesProjectId, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:features) { table(:project_features) }
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ before do
+ (1..3).each do |i|
+ namespaces.create(id: i, name: "ns-test-#{i}", path: "ns-test-i#{i}")
+ projects.create!(id: i, name: "test-#{i}", path: "test-#{i}", namespace_id: i)
+ end
+
+ features.create!(id: 1, project_id: 1)
+ features.create!(id: 2, project_id: 1)
+ features.create!(id: 3, project_id: 2)
+ features.create!(id: 4, project_id: 2)
+ features.create!(id: 5, project_id: 2)
+ features.create!(id: 6, project_id: 3)
+ end
+
+ it 'creates a unique index and removes duplicates' do
+ expect(migration.index_exists?(:project_features, :project_id, unique: false, name: 'index_project_features_on_project_id')).to be true
+
+ expect { migration.up }.to change { features.count }.from(6).to(3)
+
+ expect(migration.index_exists?(:project_features, :project_id, unique: true, name: 'index_project_features_on_project_id')).to be true
+ expect(migration.index_exists?(:project_features, :project_id, name: 'index_project_features_on_project_id_unique')).to be false
+
+ project_1_features = features.where(project_id: 1)
+ expect(project_1_features.count).to eq(1)
+ expect(project_1_features.first.id).to eq(2)
+
+ project_2_features = features.where(project_id: 2)
+ expect(project_2_features.count).to eq(1)
+ expect(project_2_features.first.id).to eq(5)
+
+ project_3_features = features.where(project_id: 3)
+ expect(project_3_features.count).to eq(1)
+ expect(project_3_features.first.id).to eq(6)
+ end
+ end
+
+ describe '#down' do
+ it 'restores the original index' do
+ migration.up
+
+ expect(migration.index_exists?(:project_features, :project_id, unique: true, name: 'index_project_features_on_project_id')).to be true
+
+ migration.down
+
+ expect(migration.index_exists?(:project_features, :project_id, unique: false, name: 'index_project_features_on_project_id')).to be true
+ expect(migration.index_exists?(:project_features, :project_id, name: 'index_project_features_on_project_id_old')).to be false
+ end
+ end
+end
diff --git a/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
new file mode 100644
index 00000000000..b8c3a3eda4e
--- /dev/null
+++ b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180425131009_assure_commits_count_for_merge_request_diff.rb')
+
+describe AssureCommitsCountForMergeRequestDiff, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are still unmigrated commit_counts afterwards' do
+ let(:namespaces) { table('namespaces') }
+ let(:projects) { table('projects') }
+ let(:merge_requests) { table('merge_requests') }
+ let(:diffs) { table('merge_request_diffs') }
+
+ before do
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+ merge_request = merge_requests.create!(source_branch: 'x', target_branch: 'y', target_project_id: project.id)
+ diffs.create!(commits_count: nil, merge_request_id: merge_request.id)
+ diffs.create!(commits_count: nil, merge_request_id: merge_request.id)
+ end
+
+ it 'migrates commit_counts sequentially in batches' do
+ migrate!
+
+ expect(migration).to have_received(:perform).once
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_build_stage_migration_spec.rb b/spec/migrations/cleanup_build_stage_migration_spec.rb
new file mode 100644
index 00000000000..4d4d02aaa94
--- /dev/null
+++ b/spec/migrations/cleanup_build_stage_migration_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180420010616_cleanup_build_stage_migration.rb')
+
+describe CleanupBuildStageMigration, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::MigrateBuildStage)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are pending background migrations' do
+ it 'processes pending jobs synchronously' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker
+ .perform_in(2.minutes, 'MigrateBuildStage', [1, 1])
+ BackgroundMigrationWorker
+ .perform_async('MigrateBuildStage', [1, 1])
+
+ migrate!
+
+ expect(migration).to have_received(:perform).with(1, 1).twice
+ end
+ end
+ end
+
+ context 'when there are no background migrations pending' do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ migrate!
+
+ expect(migration).not_to have_received(:perform)
+ end
+ end
+ end
+
+ context 'when there are still unmigrated builds present' do
+ let(:builds) { table('ci_builds') }
+
+ before do
+ builds.create!(name: 'test:1', ref: 'master')
+ builds.create!(name: 'test:2', ref: 'master')
+ end
+
+ it 'migrates stages sequentially in batches' do
+ expect(builds.all).to all(have_attributes(stage_id: nil))
+
+ migrate!
+
+ expect(migration).to have_received(:perform).once
+ end
+ end
+end
diff --git a/spec/migrations/create_missing_namespace_for_internal_users_spec.rb b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb
new file mode 100644
index 00000000000..ac3a4b1f68f
--- /dev/null
+++ b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180413022611_create_missing_namespace_for_internal_users.rb')
+
+describe CreateMissingNamespaceForInternalUsers, :migration do
+ let(:users) { table(:users) }
+ let(:namespaces) { table(:namespaces) }
+ let(:routes) { table(:routes) }
+
+ internal_user_types = [:ghost]
+ internal_user_types << :support_bot if ActiveRecord::Base.connection.column_exists?(:users, :support_bot)
+
+ internal_user_types.each do |attr|
+ context "for #{attr} user" do
+ let(:internal_user) do
+ users.create!(email: 'test@example.com', projects_limit: 100, username: 'test', attr => true)
+ end
+
+ it 'creates the missing namespace' do
+ expect(namespaces.find_by(owner_id: internal_user.id)).to be_nil
+
+ migrate!
+
+ namespace = Namespace.find_by(type: nil, owner_id: internal_user.id)
+ route = namespace.route
+
+ expect(namespace.path).to eq(route.path)
+ expect(namespace.name).to eq(route.name)
+ end
+
+ it 'sets notification email' do
+ users.update(internal_user.id, notification_email: nil)
+
+ expect(users.find(internal_user.id).notification_email).to be_nil
+
+ migrate!
+
+ user = users.find(internal_user.id)
+ expect(user.notification_email).to eq(user.email)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/migrate_import_attributes_data_from_projects_to_project_mirror_data_spec.rb b/spec/migrations/migrate_import_attributes_data_from_projects_to_project_mirror_data_spec.rb
new file mode 100644
index 00000000000..972c6dffc6f
--- /dev/null
+++ b/spec/migrations/migrate_import_attributes_data_from_projects_to_project_mirror_data_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180502134117_migrate_import_attributes_data_from_projects_to_project_mirror_data.rb')
+
+describe MigrateImportAttributesDataFromProjectsToProjectMirrorData, :sidekiq, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:import_state) { table(:project_mirror_data) }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ namespaces.create(id: 1, name: 'gitlab-org', path: 'gitlab-org')
+
+ projects.create!(id: 1, namespace_id: 1, name: 'gitlab1',
+ path: 'gitlab1', import_error: "foo", import_status: :started,
+ import_url: generate(:url))
+ projects.create!(id: 2, namespace_id: 1, name: 'gitlab2',
+ path: 'gitlab2', import_error: "bar", import_status: :failed,
+ import_url: generate(:url))
+ projects.create!(id: 3, namespace_id: 1, name: 'gitlab3', path: 'gitlab3', import_status: :none, import_url: generate(:url))
+ end
+
+ it 'schedules delayed background migrations in batches in bulk' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ expect(projects.where.not(import_status: :none).count).to eq(2)
+
+ subject.up
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq 2
+ expect(described_class::UP_MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1)
+ expect(described_class::UP_MIGRATION).to be_scheduled_delayed_migration(10.minutes, 2, 2)
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ import_state.create!(id: 1, project_id: 1, status: :started)
+ import_state.create!(id: 2, project_id: 2, status: :started)
+ end
+
+ it 'schedules delayed background migrations in batches in bulk for rollback' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ expect(import_state.where.not(status: :none).count).to eq(2)
+
+ subject.down
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq 2
+ expect(described_class::DOWN_MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1)
+ expect(described_class::DOWN_MIGRATION).to be_scheduled_delayed_migration(10.minutes, 2, 2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_stages_index_migration_spec.rb b/spec/migrations/schedule_stages_index_migration_spec.rb
new file mode 100644
index 00000000000..710264da375
--- /dev/null
+++ b/spec/migrations/schedule_stages_index_migration_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180420080616_schedule_stages_index_migration')
+
+describe ScheduleStagesIndexMigration, :sidekiq, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:stages) { table(:ci_stages) }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+
+ namespaces.create(id: 12, name: 'gitlab-org', path: 'gitlab-org')
+ projects.create!(id: 123, namespace_id: 12, name: 'gitlab', path: 'gitlab')
+ pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
+ stages.create!(id: 121, project_id: 123, pipeline_id: 1, name: 'build')
+ stages.create!(id: 122, project_id: 123, pipeline_id: 1, name: 'test')
+ stages.create!(id: 123, project_id: 123, pipeline_id: 1, name: 'deploy')
+ end
+
+ it 'schedules delayed background migrations in batches' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ expect(stages.all).to all(have_attributes(position: be_nil))
+
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 121, 121)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 122, 122)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 123, 123)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
new file mode 100644
index 00000000000..129b2f92683
--- /dev/null
+++ b/spec/models/active_session_spec.rb
@@ -0,0 +1,216 @@
+require 'rails_helper'
+
+RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
+ let(:user) do
+ create(:user).tap do |user|
+ user.current_sign_in_at = Time.current
+ end
+ end
+
+ let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') }
+
+ let(:request) do
+ double(:request, {
+ user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \
+ '(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]',
+ ip: '127.0.0.1',
+ session: session
+ })
+ end
+
+ describe '#current?' do
+ it 'returns true if the active session matches the current session' do
+ active_session = ActiveSession.new(session_id: '6919a6f1bb119dd7396fadc38fd18d0d')
+
+ expect(active_session.current?(session)).to be true
+ end
+
+ it 'returns false if the active session does not match the current session' do
+ active_session = ActiveSession.new(session_id: '59822c7d9fcdfa03725eff41782ad97d')
+
+ expect(active_session.current?(session)).to be false
+ end
+
+ it 'returns false if the session id is nil' do
+ active_session = ActiveSession.new(session_id: nil)
+ session = double(:session, id: nil)
+
+ expect(active_session.current?(session)).to be false
+ end
+ end
+
+ describe '.list' do
+ it 'returns all sessions by user' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
+ redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' }))
+ redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
+
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ %w[
+ 6919a6f1bb119dd7396fadc38fd18d0d
+ 59822c7d9fcdfa03725eff41782ad97d
+ ]
+ )
+ end
+
+ expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
+ end
+
+ it 'does not return obsolete entries and cleans them up' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
+
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ %w[
+ 6919a6f1bb119dd7396fadc38fd18d0d
+ 59822c7d9fcdfa03725eff41782ad97d
+ ]
+ )
+ end
+
+ expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
+ end
+ end
+
+ it 'returns an empty array if the use does not have any active session' do
+ expect(ActiveSession.list(user)).to eq []
+ end
+ end
+
+ describe '.set' do
+ it 'sets a new redis entry for the user session and a lookup entry' do
+ ActiveSession.set(user, request)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each.to_a).to match_array [
+ "session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d",
+ "session:lookup:user:gitlab:#{user.id}"
+ ]
+ end
+ end
+
+ it 'adds timestamps and information from the request' do
+ Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
+ ActiveSession.set(user, request)
+
+ session = ActiveSession.list(user)
+
+ expect(session.count).to eq 1
+ expect(session.first).to have_attributes(
+ ip_address: '127.0.0.1',
+ browser: 'Mobile Safari',
+ os: 'iOS',
+ device_name: 'iPhone 6',
+ device_type: 'smartphone',
+ created_at: Time.zone.parse('2018-03-12 09:06'),
+ updated_at: Time.zone.parse('2018-03-12 09:06'),
+ session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
+ )
+ end
+ end
+
+ it 'keeps the created_at from the login on consecutive requests' do
+ now = Time.zone.parse('2018-03-12 09:06')
+
+ Timecop.freeze(now) do
+ ActiveSession.set(user, request)
+
+ Timecop.freeze(now + 1.minute) do
+ ActiveSession.set(user, request)
+
+ session = ActiveSession.list(user)
+
+ expect(session.first).to have_attributes(
+ created_at: Time.zone.parse('2018-03-12 09:06'),
+ updated_at: Time.zone.parse('2018-03-12 09:07')
+ )
+ end
+ end
+ end
+ end
+
+ describe '.destroy' do
+ it 'removes the entry associated with the currently killed user session' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", '')
+ redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
+ end
+
+ ActiveSession.destroy(user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
+ "session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d",
+ "session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358"
+ ]
+ end
+ end
+
+ it 'removes the lookup entry' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
+ end
+
+ ActiveSession.destroy(user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
+ end
+ end
+
+ it 'removes the devise session' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ end
+
+ ActiveSession.destroy(user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
+ end
+ end
+
+ it 'does not remove the devise session if the active session could not be found' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ end
+
+ other_user = create(:user)
+
+ ActiveSession.destroy(other_user, request.session.id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "session:gitlab:*").to_a).not_to be_empty
+ end
+ end
+ end
+
+ describe '.cleanup' do
+ it 'removes obsolete lookup entries' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
+ end
+
+ ActiveSession.cleanup(user)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
+ end
+ end
+
+ it 'does not bail if there are no lookup entries' do
+ ActiveSession.cleanup(user)
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 56b5d616284..77b07cf1ac9 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -3,33 +3,10 @@ require 'rails_helper'
describe Appearance do
subject { build(:appearance) }
- it { is_expected.to be_valid }
+ it { include(CacheableAttributes) }
+ it { expect(described_class.current_without_cache).to eq(described_class.first) }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
-
- describe '.current', :use_clean_rails_memory_store_caching do
- let!(:appearance) { create(:appearance) }
-
- it 'returns the current appearance row' do
- expect(described_class.current).to eq(appearance)
- end
-
- it 'caches the result' do
- expect(described_class).to receive(:first).once
-
- 2.times { described_class.current }
- end
- end
-
- describe '#flush_redis_cache' do
- it 'flushes the cache in Redis' do
- appearance = create(:appearance)
-
- expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY)
-
- appearance.flush_redis_cache
- end
- end
+ it { is_expected.to have_many(:uploads) }
describe '#single_appearance_row' do
it 'adds an error when more than 1 row exists' do
@@ -41,4 +18,12 @@ describe Appearance do
expect(new_row.valid?).to eq(false)
end
end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', false do
+ let(:model_object) { create(:appearance, :with_logo) }
+ let(:upload_attribute) { :logo }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/models/application_setting/term_spec.rb b/spec/models/application_setting/term_spec.rb
new file mode 100644
index 00000000000..1eddf3c56ff
--- /dev/null
+++ b/spec/models/application_setting/term_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ApplicationSetting::Term do
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:terms) }
+ end
+
+ describe '.latest' do
+ it 'finds the latest terms' do
+ terms = create(:term)
+
+ expect(described_class.latest).to eq(terms)
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index ae2d34750a7..7e47043a1cb 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -3,6 +3,9 @@ require 'spec_helper'
describe ApplicationSetting do
let(:setting) { described_class.create_from_defaults }
+ it { include(CacheableAttributes) }
+ it { expect(described_class.current_without_cache).to eq(described_class.last) }
+
it { expect(setting).to be_valid }
it { expect(setting.uuid).to be_present }
it { expect(setting).to have_db_column(:auto_devops_enabled) }
@@ -301,31 +304,19 @@ describe ApplicationSetting do
expect(subject).to be_invalid
end
end
- end
- describe '.current' do
- context 'redis unavailable' do
- it 'returns an ApplicationSetting' do
- allow(Rails.cache).to receive(:fetch).and_call_original
- allow(described_class).to receive(:last).and_return(:last)
- expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(ArgumentError)
+ describe 'enforcing terms' do
+ it 'requires the terms to present when enforcing users to accept' do
+ subject.enforce_terms = true
- expect(described_class.current).to eq(:last)
+ expect(subject).to be_invalid
end
- end
-
- context 'when an ApplicationSetting is not yet present' do
- it 'does not cache nil object' do
- # when missing settings a nil object is returned, but not cached
- allow(described_class).to receive(:last).and_return(nil).twice
- expect(described_class.current).to be_nil
- # when the settings are set the method returns a valid object
- allow(described_class).to receive(:last).and_return(:last)
- expect(described_class.current).to eq(:last)
+ it 'is valid when terms are created' do
+ create(:term)
+ subject.enforce_terms = true
- # subsequent calls get everything from cache
- expect(described_class.current).to eq(:last)
+ expect(subject).to be_valid
end
end
end
diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
index b9946c0315a..8d11d58cfca 100644
--- a/spec/models/blob_viewer/readme_spec.rb
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe BlobViewer::Readme do
include FakeBlobHelpers
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository, :wiki_repo) }
let(:blob) { fake_blob(path: 'README.md') }
subject { described_class.new(blob) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index d7075d4a6b4..77179028ede 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -629,6 +629,14 @@ describe Ci::Build do
it { is_expected.to eq('review/host') }
end
+
+ context 'when using persisted variables' do
+ let(:build) do
+ create(:ci_build, environment: 'review/x$CI_BUILD_ID')
+ end
+
+ it { is_expected.to eq('review/x') }
+ end
end
describe '#starts_environment?' do
@@ -1270,6 +1278,46 @@ describe Ci::Build do
end
end
+ describe '#playable?' do
+ context 'when build is a manual action' do
+ context 'when build has been skipped' do
+ subject { build_stubbed(:ci_build, :manual, status: :skipped) }
+
+ it { is_expected.not_to be_playable }
+ end
+
+ context 'when build has been canceled' do
+ subject { build_stubbed(:ci_build, :manual, status: :canceled) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build is successful' do
+ subject { build_stubbed(:ci_build, :manual, status: :success) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build has failed' do
+ subject { build_stubbed(:ci_build, :manual, status: :failed) }
+
+ it { is_expected.to be_playable }
+ end
+
+ context 'when build is a manual untriggered action' do
+ subject { build_stubbed(:ci_build, :manual, status: :manual) }
+
+ it { is_expected.to be_playable }
+ end
+ end
+
+ context 'when build is not a manual action' do
+ subject { build_stubbed(:ci_build, :success) }
+
+ it { is_expected.not_to be_playable }
+ end
+ end
+
describe 'project settings' do
describe '#allow_git_fetch' do
it 'return project allow_git_fetch configuration' do
@@ -1384,29 +1432,51 @@ describe Ci::Build do
end
end
- describe '#update_project_statistics' do
- let!(:build) { create(:ci_build, artifacts_size: 23) }
-
- it 'updates project statistics when the artifact size changes' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(build.project_id, [], [:build_artifacts_size])
+ context 'when updating the build' do
+ let(:build) { create(:ci_build, artifacts_size: 23) }
+ it 'updates project statistics' do
build.artifacts_size = 42
- build.save!
+
+ expect(build).to receive(:update_project_statistics_after_save).and_call_original
+
+ expect { build.save! }
+ .to change { build.project.statistics.reload.build_artifacts_size }
+ .by(19)
end
- it 'does not update project statistics when the artifact size stays the same' do
- expect(ProjectCacheWorker).not_to receive(:perform_async)
+ context 'when the artifact size stays the same' do
+ it 'does not update project statistics' do
+ build.name = 'changed'
+
+ expect(build).not_to receive(:update_project_statistics_after_save)
+
+ build.save!
+ end
+ end
+ end
+
+ context 'when destroying the build' do
+ let!(:build) { create(:ci_build, artifacts_size: 23) }
+
+ it 'updates project statistics' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
- build.name = 'changed'
- build.save!
+ expect { build.destroy! }
+ .to change { build.project.statistics.reload.build_artifacts_size }
+ .by(-23)
end
- it 'updates project statistics when the build is destroyed' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(build.project_id, [], [:build_artifacts_size])
+ context 'when the build is destroyed due to the project being destroyed' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
- build.destroy
+ build.project.update_attributes(pending_delete: true)
+ build.project.destroy!
+ end
end
end
@@ -1463,6 +1533,7 @@ describe Ci::Build do
let(:container_registry_enabled) { false }
let(:predefined_variables) do
[
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_JOB_ID', value: build.id.to_s, public: true },
{ key: 'CI_JOB_TOKEN', value: build.token, public: false },
{ key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
@@ -1472,10 +1543,10 @@ describe Ci::Build do
{ 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.namespace.features.join(','), 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_REVISION', value: Gitlab::REVISION, 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 },
@@ -1494,9 +1565,11 @@ describe Ci::Build do
{ 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_ID', value: pipeline.id.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_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
@@ -2013,6 +2086,34 @@ describe Ci::Build do
expect(build).not_to be_persisted
end
end
+
+ context 'for deploy tokens' do
+ let(:deploy_token) { create(:deploy_token, :gitlab_deploy_token) }
+
+ let(:deploy_token_variables) do
+ [
+ { key: 'CI_DEPLOY_USER', value: deploy_token.username, public: true },
+ { key: 'CI_DEPLOY_PASSWORD', value: deploy_token.token, public: false }
+ ]
+ end
+
+ context 'when gitlab-deploy-token exists' do
+ before do
+ project.deploy_tokens << deploy_token
+ end
+
+ it 'should include deploy token variables' do
+ is_expected.to include(*deploy_token_variables)
+ end
+ end
+
+ context 'when gitlab-deploy-token does not exist' do
+ it 'should not include deploy token variables' do
+ expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil
+ expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil
+ end
+ end
+ end
end
describe '#scoped_variables' do
@@ -2061,7 +2162,9 @@ describe Ci::Build do
CI_REGISTRY_USER
CI_REGISTRY_PASSWORD
CI_REPOSITORY_URL
- CI_ENVIRONMENT_URL]
+ CI_ENVIRONMENT_URL
+ CI_DEPLOY_USER
+ CI_DEPLOY_PASSWORD]
build.scoped_variables.map { |env| env[:key] }.tap do |names|
expect(names).not_to include(*keys)
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
new file mode 100644
index 00000000000..cbcf1e55979
--- /dev/null
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -0,0 +1,396 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
+ set(:build) { create(:ci_build, :running) }
+ let(:chunk_index) { 0 }
+ let(:data_store) { :redis }
+ let(:raw_data) { nil }
+
+ let(:build_trace_chunk) do
+ described_class.new(build: build, chunk_index: chunk_index, data_store: data_store, raw_data: raw_data)
+ end
+
+ before do
+ stub_feature_flags(ci_enable_live_trace: true)
+ end
+
+ context 'FastDestroyAll' do
+ let(:parent) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: parent) }
+ let(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline, project: parent) }
+ let(:subjects) { build.trace_chunks }
+
+ it_behaves_like 'fast destroyable'
+
+ def external_data_counter
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size
+ end
+ end
+ end
+
+ describe 'CHUNK_SIZE' do
+ it 'Chunk size can not be changed without special care' do
+ expect(described_class::CHUNK_SIZE).to eq(128.kilobytes)
+ end
+ end
+
+ describe '#data' do
+ subject { build_trace_chunk.data }
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, 'Sample data in redis')
+ end
+
+ it { is_expected.to eq('Sample data in redis') }
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+ let(:raw_data) { 'Sample data in db' }
+
+ it { is_expected.to eq('Sample data in db') }
+ end
+
+ context 'when data_store is others' do
+ before do
+ build_trace_chunk.send(:write_attribute, :data_store, -1)
+ end
+
+ it { expect { subject }.to raise_error('Unsupported data store') }
+ end
+ end
+
+ describe '#set_data' do
+ subject { build_trace_chunk.send(:set_data, value) }
+
+ let(:value) { 'Sample data' }
+
+ context 'when value bytesize is bigger than CHUNK_SIZE' do
+ let(:value) { 'a' * (described_class::CHUNK_SIZE + 1) }
+
+ it { expect { subject }.to raise_error('too much data') }
+ end
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ it do
+ expect(build_trace_chunk.send(:redis_data)).to be_nil
+
+ subject
+
+ expect(build_trace_chunk.send(:redis_data)).to eq(value)
+ end
+
+ context 'when fullfilled chunk size' do
+ let(:value) { 'a' * described_class::CHUNK_SIZE }
+
+ it 'schedules stashing data' do
+ expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once
+
+ subject
+ end
+ end
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+
+ it 'sets data' do
+ expect(build_trace_chunk.raw_data).to be_nil
+
+ subject
+
+ expect(build_trace_chunk.raw_data).to eq(value)
+ expect(build_trace_chunk.persisted?).to be_truthy
+ end
+
+ context 'when raw_data is not changed' do
+ it 'does not execute UPDATE' do
+ expect(build_trace_chunk.raw_data).to be_nil
+ build_trace_chunk.save!
+
+ # First set
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to be > 0
+ expect(build_trace_chunk.raw_data).to eq(value)
+ expect(build_trace_chunk.persisted?).to be_truthy
+
+ # Second set
+ build_trace_chunk.reload
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to be(0)
+ end
+ end
+
+ context 'when fullfilled chunk size' do
+ it 'does not schedule stashing data' do
+ expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+
+ context 'when data_store is others' do
+ before do
+ build_trace_chunk.send(:write_attribute, :data_store, -1)
+ end
+
+ it { expect { subject }.to raise_error('Unsupported data store') }
+ end
+ end
+
+ describe '#truncate' do
+ subject { build_trace_chunk.truncate(offset) }
+
+ shared_examples_for 'truncates' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is 10' do
+ let(:offset) { 10 }
+
+ it 'truncates' do
+ subject
+
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset))
+ end
+ end
+ end
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it_behaves_like 'truncates'
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+ let(:raw_data) { 'Sample data in db' }
+ let(:data) { raw_data }
+
+ it_behaves_like 'truncates'
+ end
+ end
+
+ describe '#append' do
+ subject { build_trace_chunk.append(new_data, offset) }
+
+ let(:new_data) { 'Sample new data' }
+ let(:offset) { 0 }
+ let(:total_data) { data + new_data }
+
+ shared_examples_for 'appends' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) }
+
+ it { expect { subject }.to raise_error('Chunk size overflow') }
+ end
+
+ context 'when offset is EOF' do
+ let(:offset) { data.bytesize }
+
+ it 'appends' do
+ subject
+
+ expect(build_trace_chunk.data).to eq(total_data)
+ end
+ end
+
+ context 'when offset is 10' do
+ let(:offset) { 10 }
+
+ it 'appends' do
+ subject
+
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data)
+ end
+ end
+ end
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it_behaves_like 'appends'
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+ let(:raw_data) { 'Sample data in db' }
+ let(:data) { raw_data }
+
+ it_behaves_like 'appends'
+ end
+ end
+
+ describe '#size' do
+ subject { build_trace_chunk.size }
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it { is_expected.to eq(data.bytesize) }
+ end
+
+ context 'when data exists' do
+ it { is_expected.to eq(0) }
+ end
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+
+ context 'when data exists' do
+ let(:raw_data) { 'Sample data in db' }
+ let(:data) { raw_data }
+
+ it { is_expected.to eq(data.bytesize) }
+ end
+
+ context 'when data does not exist' do
+ it { is_expected.to eq(0) }
+ end
+ end
+ end
+
+ describe '#use_database!' do
+ subject { build_trace_chunk.use_database! }
+
+ context 'when data_store is redis' do
+ let(:data_store) { :redis }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in redis' }
+
+ before do
+ build_trace_chunk.send(:redis_set_data, data)
+ end
+
+ it 'stashes the data' do
+ expect(build_trace_chunk.data_store).to eq('redis')
+ expect(build_trace_chunk.send(:redis_data)).to eq(data)
+ expect(build_trace_chunk.raw_data).to be_nil
+
+ subject
+
+ expect(build_trace_chunk.data_store).to eq('db')
+ expect(build_trace_chunk.send(:redis_data)).to be_nil
+ expect(build_trace_chunk.raw_data).to eq(data)
+ end
+ end
+
+ context 'when data does not exist' do
+ it 'does not call UPDATE' do
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ end
+ end
+ end
+
+ context 'when data_store is database' do
+ let(:data_store) { :db }
+
+ it 'does not call UPDATE' do
+ expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ end
+ end
+ end
+
+ describe 'ExclusiveLock' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
+ stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1)
+ end
+
+ it 'raise an error' do
+ expect { build_trace_chunk.append('ABC', 0) }.to raise_error('Failed to obtain write lock')
+ end
+ end
+
+ describe 'deletes data in redis after a parent record destroyed' do
+ let(:project) { create(:project) }
+
+ before do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
+ create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
+ create(:ci_build, :running, :trace_live, pipeline: pipeline, project: project)
+ end
+
+ shared_examples_for 'deletes all build_trace_chunk and data in redis' do
+ it do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(3)
+ end
+
+ expect(described_class.count).to eq(3)
+
+ subject
+
+ expect(described_class.count).to eq(0)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(0)
+ end
+ end
+ end
+
+ context 'when traces are archived' do
+ let(:subject) do
+ project.builds.each do |build|
+ build.success!
+ end
+ end
+
+ it_behaves_like 'deletes all build_trace_chunk and data in redis'
+ end
+
+ context 'when project is destroyed' do
+ let(:subject) do
+ project.destroy!
+ end
+
+ it_behaves_like 'deletes all build_trace_chunk and data in redis'
+ end
+ end
+end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 1aa28434879..a3e119cbc27 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::JobArtifact do
- set(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:artifact) { create(:ci_job_artifact, :archive) }
describe "Associations" do
it { is_expected.to belong_to(:project) }
@@ -59,10 +59,32 @@ describe Ci::JobArtifact do
end
end
- describe '#set_size' do
- it 'sets the size' do
+ context 'creating the artifact' do
+ let(:project) { create(:project) }
+ let(:artifact) { create(:ci_job_artifact, :archive, project: project) }
+
+ it 'sets the size from the file size' do
expect(artifact.size).to eq(106365)
end
+
+ it 'updates the project statistics' do
+ expect { artifact }
+ .to change { project.statistics.reload.build_artifacts_size }
+ .by(106365)
+ end
+ end
+
+ context 'updating the artifact file' do
+ it 'updates the artifact size' do
+ artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ expect(artifact.size).to eq(1062)
+ end
+
+ it 'updates the project statistics' do
+ expect { artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ .to change { artifact.project.statistics.reload.build_artifacts_size }
+ .by(1062 - 106365)
+ end
end
describe '#file' do
@@ -118,4 +140,71 @@ describe Ci::JobArtifact do
is_expected.to be_nil
end
end
+
+ context 'when destroying the artifact' do
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ it 'updates the project statistics' do
+ artifact = build.job_artifacts.first
+
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ expect { artifact.destroy }
+ .to change { project.statistics.reload.build_artifacts_size }
+ .by(-106365)
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update_attributes(pending_delete: true)
+ project.destroy!
+ end
+ end
+ end
+
+ describe 'file is being stored' do
+ subject { create(:ci_job_artifact, :archive) }
+
+ context 'when object has nil store' do
+ before do
+ subject.update_column(:file_store, nil)
+ subject.reload
+ end
+
+ it 'is stored locally' do
+ expect(subject.file_store).to be(nil)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when existing object has local store' do
+ it 'is stored locally' do
+ expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
+ expect(subject.file).to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_artifacts_object_storage(direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ it 'is stored remotely' do
+ expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(subject.file).not_to be_file_storage
+ expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index dd94515b0a4..e4f4c62bd22 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -167,13 +167,39 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#persisted_variables' do
+ context 'when pipeline is not persisted yet' do
+ subject { build(:ci_pipeline).persisted_variables }
+
+ it 'does not contain some variables' do
+ keys = subject.map { |variable| variable[:key] }
+
+ expect(keys).not_to include 'CI_PIPELINE_ID'
+ end
+ end
+
+ context 'when pipeline is persisted' do
+ subject { build_stubbed(:ci_pipeline).persisted_variables }
+
+ it 'does contains persisted variables' do
+ keys = subject.map { |variable| variable[:key] }
+
+ expect(keys).to eq %w[CI_PIPELINE_ID]
+ end
+ end
+ end
+
describe '#predefined_variables' do
subject { pipeline.predefined_variables }
it 'includes all predefined variables in a valid order' do
keys = subject.map { |variable| variable[:key] }
- expect(keys).to eq %w[CI_PIPELINE_ID CI_CONFIG_PATH CI_PIPELINE_SOURCE]
+ expect(keys).to eq %w[CI_CONFIG_PATH
+ CI_PIPELINE_SOURCE
+ CI_COMMIT_MESSAGE
+ CI_COMMIT_TITLE
+ CI_COMMIT_DESCRIPTION]
end
end
@@ -774,6 +800,33 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#number_of_warnings' do
+ it 'returns the number of warnings' do
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
+
+ expect(pipeline.number_of_warnings).to eq(1)
+ end
+
+ it 'supports eager loading of the number of warnings' do
+ pipeline2 = create(:ci_empty_pipeline, status: :created, project: project)
+
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop')
+ create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop')
+
+ pipelines = project.pipelines.to_a
+
+ pipelines.each(&:number_of_warnings)
+
+ # To run the queries we need to actually use the lazy objects, which we do
+ # by just sending "to_i" to them.
+ amount = ActiveRecord::QueryRecorder
+ .new { pipelines.each { |p| p.number_of_warnings.to_i } }
+ .count
+
+ expect(amount).to eq(1)
+ end
+ end
+
shared_context 'with some outdated pipelines' do
before do
create_pipeline(:canceled, 'ref', 'A', project)
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index ab170e6351c..0fbc934f669 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Ci::Runner do
describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) }
+ it { is_expected.to validate_presence_of(:runner_type) }
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
@@ -19,6 +20,63 @@ describe Ci::Runner do
end
end
end
+
+ context 'either_projects_or_group' do
+ let(:group) { create(:group) }
+
+ it 'disallows assigning to a group if already assigned to a group' do
+ runner = create(:ci_runner, groups: [group])
+
+ runner.groups << build(:group)
+
+ expect(runner).not_to be_valid
+ expect(runner.errors.full_messages).to eq ['Runner can only be assigned to one group']
+ end
+
+ it 'disallows assigning to a group if already assigned to a project' do
+ project = create(:project)
+ runner = create(:ci_runner, projects: [project])
+
+ runner.groups << build(:group)
+
+ expect(runner).not_to be_valid
+ expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
+ end
+
+ it 'disallows assigning to a project if already assigned to a group' do
+ runner = create(:ci_runner, groups: [group])
+
+ runner.projects << build(:project)
+
+ expect(runner).not_to be_valid
+ expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
+ end
+
+ it 'allows assigning to a group if not assigned to a group nor a project' do
+ runner = create(:ci_runner)
+
+ runner.groups << build(:group)
+
+ expect(runner).to be_valid
+ end
+
+ it 'allows assigning to a project if not assigned to a group nor a project' do
+ runner = create(:ci_runner)
+
+ runner.projects << build(:project)
+
+ expect(runner).to be_valid
+ end
+
+ it 'allows assigning to a project if already assigned to a project' do
+ project = create(:project)
+ runner = create(:ci_runner, projects: [project])
+
+ runner.projects << build(:project)
+
+ expect(runner).to be_valid
+ end
+ end
end
describe '#access_level' do
@@ -49,6 +107,80 @@ describe Ci::Runner do
end
end
+ describe '.shared' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project) }
+
+ it 'returns the shared group runner' do
+ runner = create(:ci_runner, :shared, groups: [group])
+
+ expect(described_class.shared).to eq [runner]
+ end
+
+ it 'returns the shared project runner' do
+ runner = create(:ci_runner, :shared, projects: [project])
+
+ expect(described_class.shared).to eq [runner]
+ end
+ end
+
+ describe '.belonging_to_project' do
+ it 'returns the specific project runner' do
+ # own
+ specific_project = create(:project)
+ specific_runner = create(:ci_runner, :specific, projects: [specific_project])
+
+ # other
+ other_project = create(:project)
+ create(:ci_runner, :specific, projects: [other_project])
+
+ expect(described_class.belonging_to_project(specific_project.id)).to eq [specific_runner]
+ end
+ end
+
+ describe '.belonging_to_parent_group_of_project' do
+ let(:project) { create(:project, group: group) }
+ let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner, :specific, groups: [group]) }
+ let!(:unrelated_group) { create(:group) }
+ let!(:unrelated_project) { create(:project, group: unrelated_group) }
+ let!(:unrelated_runner) { create(:ci_runner, :specific, groups: [unrelated_group]) }
+
+ it 'returns the specific group runner' do
+ expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
+ end
+
+ context 'with a parent group with a runner', :nested_groups do
+ let(:runner) { create(:ci_runner, :specific, groups: [parent_group]) }
+ let(:project) { create(:project, group: group) }
+ let(:group) { create(:group, parent: parent_group) }
+ let(:parent_group) { create(:group) }
+
+ it 'returns the group runner from the parent group' do
+ expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
+ end
+ end
+ end
+
+ describe '.owned_or_shared' do
+ it 'returns a globally shared, a project specific and a group specific runner' do
+ # group specific
+ group = create(:group)
+ project = create(:project, group: group)
+ group_runner = create(:ci_runner, :specific, groups: [group])
+
+ # project specific
+ project_runner = create(:ci_runner, :specific, projects: [project])
+
+ # globally shared
+ shared_runner = create(:ci_runner, :shared)
+
+ expect(described_class.owned_or_shared(project.id)).to contain_exactly(
+ group_runner, project_runner, shared_runner
+ )
+ end
+ end
+
describe '#display_name' do
it 'returns the description if it has a value' do
runner = FactoryBot.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
@@ -67,16 +199,30 @@ describe Ci::Runner do
end
describe '#assign_to' do
- let!(:project) { FactoryBot.create :project }
- let!(:shared_runner) { FactoryBot.create(:ci_runner, :shared) }
+ let!(:project) { FactoryBot.create(:project) }
- before do
- shared_runner.assign_to(project)
+ subject { runner.assign_to(project) }
+
+ context 'with shared_runner' do
+ let!(:runner) { FactoryBot.create(:ci_runner, :shared) }
+
+ it 'transitions shared runner to project runner and assigns project' do
+ subject
+ expect(runner).to be_specific
+ expect(runner).to be_project_type
+ expect(runner.projects).to eq([project])
+ expect(runner.only_for?(project)).to be_truthy
+ end
end
- it { expect(shared_runner).to be_specific }
- it { expect(shared_runner.projects).to eq([project]) }
- it { expect(shared_runner.only_for?(project)).to be_truthy }
+ context 'with group runner' do
+ let!(:runner) { FactoryBot.create(:ci_runner, runner_type: :group_type) }
+
+ it 'raises an error' do
+ expect { subject }
+ .to raise_error(ArgumentError, 'Transitioning a group runner to a project runner is not supported')
+ end
+ end
end
describe '.online' do
@@ -163,7 +309,9 @@ describe Ci::Runner do
describe '#can_pick?' do
let(:pipeline) { create(:ci_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, tag_list: tag_list, run_untagged: run_untagged) }
+ let(:tag_list) { [] }
+ let(:run_untagged) { true }
subject { runner.can_pick?(build) }
@@ -171,6 +319,13 @@ describe Ci::Runner do
build.project.runners << runner
end
+ context 'a different runner' do
+ it 'cannot handle builds' do
+ other_runner = create(:ci_runner)
+ expect(other_runner.can_pick?(build)).to be_falsey
+ end
+ end
+
context 'when runner does not have tags' do
it 'can handle builds without tags' do
expect(runner.can_pick?(build)).to be_truthy
@@ -184,9 +339,7 @@ describe Ci::Runner do
end
context 'when runner has tags' do
- before do
- runner.tag_list = %w(bb cc)
- end
+ let(:tag_list) { %w(bb cc) }
shared_examples 'tagged build picker' do
it 'can handle build with matching tags' do
@@ -211,9 +364,7 @@ describe Ci::Runner do
end
context 'when runner cannot pick untagged jobs' do
- before do
- runner.run_untagged = false
- end
+ let(:run_untagged) { false }
it 'cannot handle builds without tags' do
expect(runner.can_pick?(build)).to be_falsey
@@ -224,8 +375,9 @@ describe Ci::Runner do
end
context 'when runner is shared' do
+ let(:runner) { create(:ci_runner, :shared) }
+
before do
- runner.is_shared = true
build.project.runners = []
end
@@ -234,9 +386,7 @@ describe Ci::Runner do
end
context 'when runner is locked' do
- before do
- runner.locked = true
- end
+ let(:runner) { create(:ci_runner, :shared, locked: true) }
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
@@ -260,6 +410,17 @@ describe Ci::Runner do
expect(runner.can_pick?(build)).to be_falsey
end
end
+
+ context 'when runner is assigned to a group' do
+ before do
+ build.project.runners = []
+ runner.groups << create(:group, projects: [build.project])
+ end
+
+ it 'can handle builds' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+ end
end
context 'when access_level of runner is not_protected' do
@@ -465,62 +626,26 @@ describe Ci::Runner do
end
describe '.assignable_for' do
- let(:runner) { create(:ci_runner) }
+ let!(:unlocked_project_runner) { create(:ci_runner, runner_type: :project_type, projects: [project]) }
+ let!(:locked_project_runner) { create(:ci_runner, runner_type: :project_type, locked: true, projects: [project]) }
+ let!(:group_runner) { create(:ci_runner, runner_type: :group_type) }
+ let!(:instance_runner) { create(:ci_runner, :shared) }
let(:project) { create(:project) }
let(:another_project) { create(:project) }
- before do
- project.runners << runner
- end
-
- context 'with shared runners' do
- before do
- runner.update(is_shared: true)
- end
-
- context 'does not give owned runner' do
- subject { described_class.assignable_for(project) }
-
- it { is_expected.to be_empty }
- end
-
- context 'does not give shared runner' do
- subject { described_class.assignable_for(another_project) }
-
- it { is_expected.to be_empty }
- end
- end
-
- context 'with unlocked runner' do
- context 'does not give owned runner' do
- subject { described_class.assignable_for(project) }
-
- it { is_expected.to be_empty }
- end
+ context 'with already assigned project' do
+ subject { described_class.assignable_for(project) }
- context 'does give a specific runner' do
- subject { described_class.assignable_for(another_project) }
-
- it { is_expected.to contain_exactly(runner) }
- end
+ it { is_expected.to be_empty }
end
- context 'with locked runner' do
- before do
- runner.update(locked: true)
- end
-
- context 'does not give owned runner' do
- subject { described_class.assignable_for(project) }
-
- it { is_expected.to be_empty }
- end
+ context 'with a different project' do
+ subject { described_class.assignable_for(another_project) }
- context 'does not give a locked runner' do
- subject { described_class.assignable_for(another_project) }
-
- it { is_expected.to be_empty }
- end
+ it { is_expected.to include(unlocked_project_runner) }
+ it { is_expected.not_to include(group_runner) }
+ it { is_expected.not_to include(locked_project_runner) }
+ it { is_expected.not_to include(instance_runner) }
end
end
@@ -583,4 +708,76 @@ describe Ci::Runner do
expect(described_class.search(runner.description.upcase)).to eq([runner])
end
end
+
+ describe '#assigned_to_group?' do
+ subject { runner.assigned_to_group? }
+
+ context 'when project runner' do
+ let(:runner) { create(:ci_runner, description: 'Project runner', projects: [project]) }
+ let(:project) { create(:project) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when shared runner' do
+ let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when group runner' do
+ let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#assigned_to_project?' do
+ subject { runner.assigned_to_project? }
+
+ context 'when group runner' do
+ let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
+ let(:group) { create(:group) }
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when shared runner' do
+ let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when project runner' do
+ let(:runner) { create(:ci_runner, description: 'Group runner', projects: [project]) }
+ let(:project) { create(:project) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#pick_build!' do
+ context 'runner can pick the build' do
+ it 'calls #tick_runner_queue' do
+ ci_build = build(:ci_build)
+ runner = build(:ci_runner)
+ allow(runner).to receive(:can_pick?).with(ci_build).and_return(true)
+
+ expect(runner).to receive(:tick_runner_queue)
+
+ runner.pick_build!(ci_build)
+ end
+ end
+
+ context 'runner cannot pick the build' do
+ it 'does not call #tick_runner_queue' do
+ ci_build = build(:ci_build)
+ runner = build(:ci_runner)
+ allow(runner).to receive(:can_pick?).with(ci_build).and_return(false)
+
+ expect(runner).not_to receive(:tick_runner_queue)
+
+ runner.pick_build!(ci_build)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 586d073eb5e..a00db1d2bfc 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -51,7 +51,7 @@ describe Ci::Stage, :models do
end
end
- describe 'update_status' do
+ describe '#update_status' do
context 'when stage objects needs to be updated' do
before do
create(:ci_build, :success, stage_id: stage.id)
@@ -87,4 +87,36 @@ describe Ci::Stage, :models do
end
end
end
+
+ describe '#index' do
+ context 'when stage has been imported and does not have position index set' do
+ before do
+ stage.update_column(:position, nil)
+ end
+
+ context 'when stage has statuses' do
+ before do
+ create(:ci_build, :running, stage_id: stage.id, stage_idx: 10)
+ end
+
+ it 'recalculates index before updating status' do
+ expect(stage.reload.position).to be_nil
+
+ stage.update_status
+
+ expect(stage.reload.position).to eq 10
+ end
+ end
+
+ context 'when stage does not have statuses' do
+ it 'fallbacks to zero' do
+ expect(stage.reload.position).to be_nil
+
+ stage.update_status
+
+ expect(stage.reload.position).to eq 0
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index aeca6ee903a..d2302583ac8 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -79,12 +79,22 @@ describe Clusters::Applications::Prometheus do
end
it 'creates proper url' do
- expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/proxy/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80')
+ expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80/proxy')
end
it 'copies options and headers from kube client to proxy client' do
expect(subject.prometheus_client.options).to eq(kube_client.rest_client.options.merge(headers: kube_client.headers))
end
+
+ context 'when cluster is not reachable' do
+ before do
+ allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ end
+
+ it 'returns nil' do
+ expect(subject.prometheus_client).to be_nil
+ end
+ end
end
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 64d995a73c1..3ef59457c5f 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -55,13 +55,9 @@ describe Clusters::Applications::Runner do
context 'without a runner' do
let(:project) { create(:project) }
- let(:cluster) { create(:cluster) }
+ let(:cluster) { create(:cluster, projects: [project]) }
let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) }
- before do
- cluster.projects << project
- end
-
it 'creates a runner' do
expect do
subject
@@ -74,9 +70,8 @@ describe Clusters::Applications::Runner do
it 'assigns the new runner to runner' do
subject
- gitlab_runner.reload
- expect(gitlab_runner.runner).not_to be_nil
+ expect(gitlab_runner.reload.runner).to be_project_type
end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index add481b8096..ab7f89f9bf4 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -239,17 +239,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_nil }
end
- context 'when kubernetes responds with valid pods' do
+ context 'when kubernetes responds with valid pods and deployments' do
before do
stub_kubeclient_pods
+ stub_kubeclient_deployments
end
- it { is_expected.to eq(pods: [kube_pod]) }
+ it { is_expected.to include(pods: [kube_pod]) }
end
context 'when kubernetes responds with 500s' do
before do
stub_kubeclient_pods(status: 500)
+ stub_kubeclient_deployments(status: 500)
end
it { expect { subject }.to raise_error(Kubeclient::HttpError) }
@@ -258,9 +260,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do
before do
stub_kubeclient_pods(status: 404)
+ stub_kubeclient_deployments(status: 404)
end
- it { is_expected.to eq(pods: []) }
+ it { is_expected.to include(pods: []) }
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 4e6b037a720..090f91168ad 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -52,22 +52,98 @@ describe Commit do
end
end
- describe '#author' do
+ describe '#author', :request_store do
it 'looks up the author in a case-insensitive way' do
user = create(:user, email: commit.author_email.upcase)
expect(commit.author).to eq(user)
end
- it 'caches the author', :request_store do
+ it 'caches the author' do
user = create(:user, email: commit.author_email)
- expect(User).to receive(:find_by_any_email).and_call_original
expect(commit.author).to eq(user)
+
key = "Commit:author:#{commit.author_email.downcase}"
- expect(RequestStore.store[key]).to eq(user)
+ expect(RequestStore.store[key]).to eq(user)
expect(commit.author).to eq(user)
end
+
+ context 'using eager loading' do
+ let!(:alice) { create(:user, email: 'alice@example.com') }
+ let!(:bob) { create(:user, email: 'hunter2@example.com') }
+
+ let(:alice_commit) do
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'alice@example.com'
+ end
+ end
+
+ let(:bob_commit) do
+ # The commit for Bob uses one of his alternative Emails, instead of the
+ # primary one.
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'bob@example.com'
+ end
+ end
+
+ let(:eve_commit) do
+ described_class.new(RepoHelpers.sample_commit, project).tap do |c|
+ c.author_email = 'eve@example.com'
+ end
+ end
+
+ let!(:commits) { [alice_commit, bob_commit, eve_commit] }
+
+ before do
+ create(:email, user: bob, email: 'bob@example.com')
+ end
+
+ it 'executes only two SQL queries' do
+ recorder = ActiveRecord::QueryRecorder.new do
+ # Running this first ensures we don't run one query for every
+ # commit.
+ commits.each(&:lazy_author)
+
+ # This forces the execution of the SQL queries necessary to load the
+ # data.
+ commits.each { |c| c.author.try(:id) }
+ end
+
+ expect(recorder.count).to eq(2)
+ end
+
+ it "preloads the authors for Commits matching a user's primary Email" do
+ commits.each(&:lazy_author)
+
+ expect(alice_commit.author).to eq(alice)
+ end
+
+ it "preloads the authors for Commits using a User's alternative Email" do
+ commits.each(&:lazy_author)
+
+ expect(bob_commit.author).to eq(bob)
+ end
+
+ it 'sets the author to Nil if an author could not be found for a Commit' do
+ commits.each(&:lazy_author)
+
+ expect(eve_commit.author).to be_nil
+ end
+
+ it 'does not execute SQL queries once the authors are preloaded' do
+ commits.each(&:lazy_author)
+ commits.each { |c| c.author.try(:id) }
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ alice_commit.author
+ bob_commit.author
+ eve_commit.author
+ end
+
+ expect(recorder.count).to be_zero
+ end
+ end
end
describe '#to_reference' do
@@ -182,7 +258,6 @@ eos
it { is_expected.to respond_to(:date) }
it { is_expected.to respond_to(:diffs) }
it { is_expected.to respond_to(:id) }
- it { is_expected.to respond_to(:to_patch) }
end
describe '#closes_issues' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 2ed29052dc1..f3f2bc28d2c 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -565,4 +565,10 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
end
+
+ describe '#present' do
+ subject { commit_status.present }
+
+ it { is_expected.to be_a(CommitStatusPresenter) }
+ end
end
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
index 3696e6f62fd..9faf21bfbbd 100644
--- a/spec/models/concerns/avatarable_spec.rb
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Avatarable do
- set(:project) { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ let(:project) { create(:project, :with_avatar) }
let(:gitlab_host) { "https://gitlab.example.com" }
let(:relative_url_root) { "/gitlab" }
@@ -37,11 +37,23 @@ describe Avatarable do
project.visibility_level = visibility_level
end
- let(:avatar_path) { (avatar_path_prefix + [project.avatar.url]).join }
+ let(:avatar_path) { (avatar_path_prefix + [project.avatar.local_url]).join }
it 'returns the expected avatar path' do
expect(project.avatar_path(only_path: only_path)).to eq(avatar_path)
end
+
+ context "when avatar is stored remotely" do
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+
+ project.avatar.migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ it 'returns the expected avatar path' do
+ expect(project.avatar_url(only_path: only_path)).to eq(avatar_path)
+ end
+ end
end
end
end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
new file mode 100644
index 00000000000..49e4b23ebc7
--- /dev/null
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+
+describe CacheableAttributes do
+ let(:minimal_test_class) do
+ Class.new do
+ include ActiveModel::Model
+ extend ActiveModel::Callbacks
+ define_model_callbacks :commit
+ include CacheableAttributes
+
+ def self.name
+ 'TestClass'
+ end
+
+ def self.first
+ @_first ||= new('foo' => 'a')
+ end
+
+ def self.last
+ @_last ||= new('foo' => 'a', 'bar' => 'b')
+ end
+
+ attr_accessor :attributes
+
+ def initialize(attrs = {})
+ @attributes = attrs
+ end
+ end
+ end
+
+ shared_context 'with defaults' do
+ before do
+ minimal_test_class.define_singleton_method(:defaults) do
+ { foo: 'a', bar: 'b', baz: 'c' }
+ end
+ end
+ end
+
+ describe '.current_without_cache' do
+ it 'defaults to last' do
+ expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.last)
+ end
+
+ it 'can be overriden' do
+ minimal_test_class.define_singleton_method(:current_without_cache) do
+ first
+ end
+
+ expect(minimal_test_class.current_without_cache).to eq(minimal_test_class.first)
+ end
+ end
+
+ describe '.cache_key' do
+ it 'excludes cache attributes' do
+ expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json")
+ end
+ end
+
+ describe '.defaults' do
+ it 'defaults to {}' do
+ expect(minimal_test_class.defaults).to eq({})
+ end
+
+ context 'with defaults defined' do
+ include_context 'with defaults'
+
+ it 'can be overriden' do
+ expect(minimal_test_class.defaults).to eq({ foo: 'a', bar: 'b', baz: 'c' })
+ end
+ end
+ end
+
+ describe '.build_from_defaults' do
+ include_context 'with defaults'
+
+ context 'without any attributes given' do
+ it 'intializes a new object with the defaults' do
+ expect(minimal_test_class.build_from_defaults).not_to be_persisted
+ end
+ end
+
+ context 'without attributes given' do
+ it 'intializes a new object with the given attributes merged into the defaults' do
+ expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d')
+ end
+ end
+ end
+
+ describe '.current', :use_clean_rails_memory_store_caching do
+ context 'redis unavailable' do
+ it 'returns an uncached record' do
+ allow(minimal_test_class).to receive(:last).and_return(:last)
+ expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError)
+
+ expect(minimal_test_class.current).to eq(:last)
+ end
+ end
+
+ context 'when a record is not yet present' do
+ it 'does not cache nil object' do
+ # when missing settings a nil object is returned, but not cached
+ allow(minimal_test_class).to receive(:last).twice.and_return(nil)
+
+ expect(minimal_test_class.current).to be_nil
+ expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false)
+ end
+
+ it 'cache non-nil object' do
+ # when the settings are set the method returns a valid object
+ allow(minimal_test_class).to receive(:last).and_call_original
+
+ expect(minimal_test_class.current).to eq(minimal_test_class.last)
+ expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true)
+
+ # subsequent calls retrieve the record from the cache
+ last_record = minimal_test_class.last
+ expect(minimal_test_class).not_to receive(:last)
+ expect(minimal_test_class.current.attributes).to eq(last_record.attributes)
+ end
+ end
+ end
+
+ describe '.cached', :use_clean_rails_memory_store_caching do
+ context 'when cache is cold' do
+ it 'returns nil' do
+ expect(minimal_test_class.cached).to be_nil
+ end
+ end
+
+ context 'when cached settings do not include the latest defaults' do
+ before do
+ Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json)
+ minimal_test_class.define_singleton_method(:defaults) do
+ { foo: 'a', bar: 'b', baz: 'c' }
+ end
+ end
+
+ it 'includes attributes from defaults' do
+ expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo])
+ end
+ end
+ end
+
+ describe '#cache!', :use_clean_rails_memory_store_caching do
+ let(:appearance_record) { create(:appearance) }
+
+ it 'caches the attributes' do
+ appearance_record.cache!
+
+ expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json)
+ end
+ end
+end
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 30572ce9332..8cd129dc851 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe DiscussionOnDiff do
- subject { create(:diff_note_on_merge_request).to_discussion }
+ subject { create(:diff_note_on_merge_request, line_number: 18).to_discussion }
describe "#truncated_diff_lines" do
let(:truncated_lines) { subject.truncated_diff_lines }
diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb
index c163fb01a81..28352d8c961 100644
--- a/spec/models/concerns/group_descendant_spec.rb
+++ b/spec/models/concerns/group_descendant_spec.rb
@@ -79,9 +79,24 @@ describe GroupDescendant, :nested_groups do
expect(described_class.build_hierarchy(groups)).to eq(expected_hierarchy)
end
+ it 'tracks the exception when a parent was not preloaded' do
+ expect(Gitlab::Sentry).to receive(:track_exception).and_call_original
+
+ expect { GroupDescendant.build_hierarchy([subsub_group]) }.to raise_error(ArgumentError)
+ end
+
+ it 'recovers if a parent was not reloaded by querying for the parent' do
+ expected_hierarchy = { parent => { subgroup => subsub_group } }
+
+ # this does not raise in production, so stubbing it here.
+ allow(Gitlab::Sentry).to receive(:track_exception)
+
+ expect(GroupDescendant.build_hierarchy([subsub_group])).to eq(expected_hierarchy)
+ end
+
it 'raises an error if not all elements were preloaded' do
expect { described_class.build_hierarchy([subsub_group]) }
- .to raise_error('parent was not preloaded')
+ .to raise_error(/was not preloaded/)
end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 05693f067e1..bd6bf5b0712 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -266,6 +266,19 @@ describe Issuable do
end
end
+ describe '#time_estimate=' do
+ it 'coerces the value below Gitlab::Database::MAX_INT_VALUE' do
+ expect { issue.time_estimate = 100 }.to change { issue.time_estimate }.to(100)
+ expect { issue.time_estimate = Gitlab::Database::MAX_INT_VALUE + 100 }.to change { issue.time_estimate }.to(Gitlab::Database::MAX_INT_VALUE)
+ end
+
+ it 'skips coercion for not Integer values' do
+ expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
+ expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError)
+ expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError)
+ end
+ end
+
describe '#to_hook_data' do
let(:builder) { double }
@@ -495,6 +508,14 @@ describe Issuable do
expect(issue.total_time_spent).to eq(1800)
end
+
+ it 'updates issues updated_at' do
+ issue
+
+ Timecop.travel(1.minute.from_now) do
+ expect { spend_time(1800) }.to change { issue.updated_at }
+ end
+ end
end
context 'substracting time' do
@@ -510,9 +531,13 @@ describe Issuable do
context 'when time to substract exceeds the total time spent' do
it 'raise a validation error' do
- expect do
- spend_time(-3600)
- end.to raise_error(ActiveRecord::RecordInvalid)
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ expect do
+ spend_time(-3600)
+ end.to raise_error(ActiveRecord::RecordInvalid)
+ end.not_to change { issue.updated_at }
+ end
end
end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index a5d505af001..4570dbb1d8e 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -29,12 +29,6 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
end
- let(:now) { Time.now.utc }
-
- around do |example|
- Timecop.freeze(now) { example.run }
- end
-
let(:calculation) { -> { 2 + 2 } }
let(:cache_key) { "foo:666" }
let(:instance) { CacheTest.new(666, &calculation) }
@@ -49,13 +43,15 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
context 'when cache is empty' do
it { is_expected.to be_nil }
- it 'queues a background worker' do
+ it 'enqueues a background worker to bootstrap the cache' do
expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
go!
end
it 'updates the cache lifespan' do
+ expect(reactive_cache_alive?(instance)).to be_falsy
+
go!
expect(reactive_cache_alive?(instance)).to be_truthy
@@ -69,6 +65,18 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it { is_expected.to eq(2) }
+ it 'does not enqueue a background worker' do
+ expect(ReactiveCachingWorker).not_to receive(:perform_async)
+
+ go!
+ end
+
+ it 'updates the cache lifespan' do
+ expect(Rails.cache).to receive(:write).with(alive_reactive_cache_key(instance), true, expires_in: anything)
+
+ go!
+ end
+
context 'and expired' do
before do
invalidate_reactive_cache(instance)
diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb
index 3d7963120b6..23c6c6233e9 100644
--- a/spec/models/concerns/redis_cacheable_spec.rb
+++ b/spec/models/concerns/redis_cacheable_spec.rb
@@ -1,21 +1,36 @@
require 'spec_helper'
describe RedisCacheable do
- let(:model) { double }
+ let(:model) do
+ Struct.new(:id, :attributes) do
+ def read_attribute(attribute)
+ attributes[attribute]
+ end
+
+ def cast_value_from_cache(attribute, cached_value)
+ cached_value
+ end
+
+ def has_attribute?(attribute)
+ attributes.has_key?(attribute)
+ end
+ end
+ end
+
+ let(:payload) { { name: 'value', time: Time.zone.now } }
+ let(:instance) { model.new(1, payload) }
+ let(:cache_key) { instance.__send__(:cache_attribute_key) }
before do
- model.extend(described_class)
- allow(model).to receive(:cache_attribute_key).and_return('key')
+ model.include(described_class)
end
describe '#cached_attribute' do
- let(:payload) { { attribute: 'value' } }
-
- subject { model.cached_attribute(payload.keys.first) }
+ subject { instance.cached_attribute(payload.keys.first) }
it 'gets the cache attribute' do
Gitlab::Redis::SharedState.with do |redis|
- expect(redis).to receive(:get).with('key')
+ expect(redis).to receive(:get).with(cache_key)
.and_return(payload.to_json)
end
@@ -24,16 +39,62 @@ describe RedisCacheable do
end
describe '#cache_attributes' do
- let(:values) { { name: 'new_name' } }
-
- subject { model.cache_attributes(values) }
+ subject { instance.cache_attributes(payload) }
it 'sets the cache attributes' do
Gitlab::Redis::SharedState.with do |redis|
- expect(redis).to receive(:set).with('key', values.to_json, anything)
+ expect(redis).to receive(:set).with(cache_key, payload.to_json, anything)
end
subject
end
end
+
+ describe '#cached_attr_reader', :clean_gitlab_redis_shared_state do
+ subject { instance.name }
+
+ before do
+ model.cached_attr_reader(:name)
+ end
+
+ context 'when there is no cached value' do
+ it 'reads the attribute' do
+ expect(instance).to receive(:read_attribute).and_call_original
+
+ expect(subject).to eq(payload[:name])
+ end
+ end
+
+ context 'when there is a cached value' do
+ it 'reads the cached value' do
+ expect(instance).not_to receive(:read_attribute)
+
+ instance.cache_attributes(payload)
+
+ expect(subject).to eq(payload[:name])
+ end
+ end
+
+ it 'always returns the latest values' do
+ expect(instance.name).to eq(payload[:name])
+
+ instance.cache_attributes(name: 'new_value')
+
+ expect(instance.name).to eq('new_value')
+ end
+ end
+
+ describe '#cast_value_from_cache' do
+ subject { instance.__send__(:cast_value_from_cache, attribute, value) }
+
+ context 'with runner contacted_at' do
+ let(:instance) { Ci::Runner.new }
+ let(:attribute) { :contacted_at }
+ let(:value) { '2018-05-07 13:53:08 UTC' }
+
+ it 'converts cache string to appropriate type' do
+ expect(subject).to be_an_instance_of(ActiveSupport::TimeWithZone)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
index 91591017587..fcb5250278e 100644
--- a/spec/models/concerns/resolvable_note_spec.rb
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -326,4 +326,12 @@ describe Note, ResolvableNote do
end
end
end
+
+ describe "#potentially_resolvable?" do
+ it 'returns false if noteable could not be found' do
+ allow(subject).to receive(:noteable).and_return(nil)
+
+ expect(subject.potentially_resolvable?).to be_falsey
+ end
+ end
end
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 21893e0cbaa..0d3beb6a6e3 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -13,33 +13,76 @@ describe ShaAttribute do
end
describe '#sha_attribute' do
- context 'when the table exists' do
+ context 'when in non-production' do
before do
- allow(model).to receive(:table_exists?).and_return(true)
+ allow(Rails.env).to receive(:production?).and_return(false)
end
- it 'defines a SHA attribute for a binary column' do
- expect(model).to receive(:attribute)
- .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
+ context 'when the table exists' do
+ before do
+ allow(model).to receive(:table_exists?).and_return(true)
+ end
- model.sha_attribute(:sha1)
+ it 'defines a SHA attribute for a binary column' do
+ expect(model).to receive(:attribute)
+ .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
+
+ model.sha_attribute(:sha1)
+ end
+
+ it 'raises ArgumentError when the column type is not :binary' do
+ expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the table does not exist' do
+ it 'allows the attribute to be added and issues a warning' do
+ allow(model).to receive(:table_exists?).and_return(false)
+
+ expect(model).not_to receive(:columns)
+ expect(model).to receive(:attribute)
+ expect(model).to receive(:warn)
+
+ model.sha_attribute(:name)
+ end
end
- it 'raises ArgumentError when the column type is not :binary' do
- expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
+ context 'when the column does not exist' do
+ it 'allows the attribute to be added and issues a warning' do
+ allow(model).to receive(:table_exists?).and_return(true)
+
+ expect(model).to receive(:columns)
+ expect(model).to receive(:attribute)
+ expect(model).to receive(:warn)
+
+ model.sha_attribute(:no_name)
+ end
+ end
+
+ context 'when other execeptions are raised' do
+ it 'logs and re-rasises the error' do
+ allow(model).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError.new('does not exist'))
+
+ expect(model).not_to receive(:columns)
+ expect(model).not_to receive(:attribute)
+ expect(Gitlab::AppLogger).to receive(:error)
+
+ expect { model.sha_attribute(:name) }.to raise_error(ActiveRecord::NoDatabaseError)
+ end
end
end
- context 'when the table does not exist' do
+ context 'when in production' do
before do
- allow(model).to receive(:table_exists?).and_return(false)
+ allow(Rails.env).to receive(:production?).and_return(true)
end
- it 'does nothing' do
+ it 'defines a SHA attribute' do
+ expect(model).not_to receive(:table_exists?)
expect(model).not_to receive(:columns)
- expect(model).not_to receive(:attribute)
+ expect(model).to receive(:attribute).with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
- model.sha_attribute(:name)
+ model.sha_attribute(:sha1)
end
end
end
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
new file mode 100644
index 00000000000..b821a84d5e0
--- /dev/null
+++ b/spec/models/concerns/sortable_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe Sortable do
+ describe '.order_by' do
+ let(:relation) { Group.all }
+
+ describe 'ordering by id' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(id: :asc)
+
+ relation.order_by('id_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(id: :desc)
+
+ relation.order_by('id_desc')
+ end
+ end
+
+ describe 'ordering by created day' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(created_at: :asc)
+
+ relation.order_by('created_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(created_at: :desc)
+
+ relation.order_by('created_desc')
+ end
+
+ it 'order by "date"' do
+ expect(relation).to receive(:reorder).with(created_at: :desc)
+
+ relation.order_by('created_date')
+ end
+ end
+
+ describe 'ordering by name' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with("lower(name) asc")
+
+ relation.order_by('name_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with("lower(name) desc")
+
+ relation.order_by('name_desc')
+ end
+ end
+
+ describe 'ordering by Updated Time' do
+ it 'ascending' do
+ expect(relation).to receive(:reorder).with(updated_at: :asc)
+
+ relation.order_by('updated_asc')
+ end
+
+ it 'descending' do
+ expect(relation).to receive(:reorder).with(updated_at: :desc)
+
+ relation.order_by('updated_desc')
+ end
+ end
+
+ it 'does not call reorder in case of unrecognized ordering' do
+ expect(relation).not_to receive(:reorder)
+
+ relation.order_by('random_ordering')
+ end
+ end
+
+ describe 'sorting groups' do
+ def ordered_group_names(order)
+ Group.all.order_by(order).map(&:name)
+ end
+
+ let!(:ref_time) { Time.parse('2018-05-01 00:00:00') }
+ let!(:group1) { create(:group, name: 'aa', id: 1, created_at: ref_time - 15.seconds, updated_at: ref_time) }
+ let!(:group2) { create(:group, name: 'AAA', id: 2, created_at: ref_time - 10.seconds, updated_at: ref_time - 5.seconds) }
+ let!(:group3) { create(:group, name: 'BB', id: 3, created_at: ref_time - 5.seconds, updated_at: ref_time - 10.seconds) }
+ let!(:group4) { create(:group, name: 'bbb', id: 4, created_at: ref_time, updated_at: ref_time - 15.seconds) }
+
+ it 'sorts groups by id' do
+ expect(ordered_group_names('id_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by name via case-insentitive comparision' do
+ expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by created_at' do
+ expect(ordered_group_names('created_asc')).to eq(%w(aa AAA BB bbb))
+ expect(ordered_group_names('created_desc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('created_date')).to eq(%w(bbb BB AAA aa))
+ end
+
+ it 'sorts groups by updated_at' do
+ expect(ordered_group_names('updated_asc')).to eq(%w(bbb BB AAA aa))
+ expect(ordered_group_names('updated_desc')).to eq(%w(aa AAA BB bbb))
+ end
+ end
+end
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
index 914730718e7..6cd2de6dcce 100644
--- a/spec/models/concerns/uniquify_spec.rb
+++ b/spec/models/concerns/uniquify_spec.rb
@@ -22,6 +22,15 @@ describe Uniquify do
expect(result).to eq('test_string2')
end
+ it 'allows to pass an initial value for the counter' do
+ start_counting_from = 2
+ uniquify = described_class.new(start_counting_from)
+
+ result = uniquify.string('test_string') { |s| s == 'test_string' }
+
+ expect(result).to eq('test_string2')
+ end
+
it 'allows passing in a base function that defines the location of the counter' do
result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s|
s == 'test__string'
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 780b200e837..f8d51a95833 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -142,4 +142,23 @@ describe DeployToken do
end
end
end
+
+ describe '.gitlab_deploy_token' do
+ let(:project) { create(:project ) }
+
+ subject { project.deploy_tokens.gitlab_deploy_token }
+
+ context 'with a gitlab deploy token associated' do
+ it 'should return the gitlab deploy token' do
+ deploy_token = create(:deploy_token, :gitlab_deploy_token, projects: [project])
+ is_expected.to eq(deploy_token)
+ end
+ end
+
+ context 'with no gitlab deploy token associated' do
+ it 'should return nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index ac30cd27e0c..aee70bcfb29 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,15 @@ describe Deployment do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe 'modules' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:deployment) }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :deployments }
+ end
+ end
+
describe 'after_create callbacks' do
let(:environment) { create(:environment) }
let(:store) { Gitlab::EtagCaching::Store.new }
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 2705421e540..8624f0daa4d 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -84,13 +84,96 @@ describe DiffNote do
end
end
- describe "#diff_file" do
- it "returns the correct diff file" do
- diff_file = subject.diff_file
+ describe '#create_diff_file callback' do
+ let(:noteable) { create(:merge_request) }
+ let(:project) { noteable.project }
- expect(diff_file.old_path).to eq(position.old_path)
- expect(diff_file.new_path).to eq(position.new_path)
- expect(diff_file.diff_refs).to eq(position.diff_refs)
+ context 'merge request' do
+ let!(:diff_note) { create(:diff_note_on_merge_request, project: project, noteable: noteable) }
+
+ it 'creates a diff note file' do
+ expect(diff_note.reload.note_diff_file).to be_present
+ end
+
+ it 'does not create diff note file if it is a reply' do
+ expect { create(:diff_note_on_merge_request, noteable: noteable, in_reply_to: diff_note) }
+ .not_to change(NoteDiffFile, :count)
+ end
+ end
+
+ context 'commit' do
+ let!(:diff_note) { create(:diff_note_on_commit, project: project) }
+
+ it 'creates a diff note file' do
+ expect(diff_note.reload.note_diff_file).to be_present
+ end
+
+ it 'does not create diff note file if it is a reply' do
+ expect { create(:diff_note_on_commit, in_reply_to: diff_note) }
+ .not_to change(NoteDiffFile, :count)
+ end
+ end
+ end
+
+ describe '#diff_file', :clean_gitlab_redis_shared_state do
+ context 'when note_diff_file association exists' do
+ it 'returns persisted diff file data' do
+ diff_file = subject.diff_file
+
+ expect(diff_file.diff.to_hash.with_indifferent_access)
+ .to include(subject.note_diff_file.to_hash)
+ end
+ end
+
+ context 'when the discussion was created in the diff' do
+ it 'returns correct diff file' do
+ diff_file = subject.diff_file
+
+ expect(diff_file.old_path).to eq(position.old_path)
+ expect(diff_file.new_path).to eq(position.new_path)
+ expect(diff_file.diff_refs).to eq(position.diff_refs)
+ end
+ end
+
+ context 'when discussion is outdated or not created in the diff' do
+ let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
+ 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: diff_refs
+ )
+ end
+
+ it 'returns the correct diff file' do
+ diff_file = subject.diff_file
+
+ expect(diff_file.old_path).to eq(position.old_path)
+ expect(diff_file.new_path).to eq(position.new_path)
+ expect(diff_file.diff_refs).to eq(position.diff_refs)
+ end
+ end
+
+ context 'note diff file creation enqueuing' do
+ it 'enqueues CreateNoteDiffFileWorker if it is the first note of a discussion' do
+ subject.note_diff_file.destroy!
+
+ expect(CreateNoteDiffFileWorker).to receive(:perform_async).with(subject.id)
+
+ subject.reload.diff_file
+ end
+
+ it 'does not enqueues CreateNoteDiffFileWorker if not first note of a discussion' do
+ mr = create(:merge_request)
+ diff_note = create(:diff_note_on_merge_request, project: mr.project, noteable: mr)
+ reply_diff_note = create(:diff_note_on_merge_request, in_reply_to: diff_note)
+
+ expect(CreateNoteDiffFileWorker).not_to receive(:perform_async).with(reply_diff_note.id)
+
+ reply_diff_note.reload.diff_file
+ end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 56161bfcc28..25d6597084c 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :stubbed_repository) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
@@ -201,7 +201,7 @@ describe Environment do
end
describe '#stop_with_action!' do
- let(:user) { create(:admin) }
+ let(:user) { create(:user) }
subject { environment.stop_with_action!(user) }
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 673049d1cc4..a3e68d2e646 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -78,4 +78,10 @@ describe GenericCommitStatus do
it { is_expected.not_to be_nil }
end
end
+
+ describe '#present' do
+ subject { generic_commit_status.present }
+
+ it { is_expected.to be_a(GenericCommitStatusPresenter) }
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index d620943693c..f83b52e8975 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -15,7 +15,7 @@ describe Group do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') }
it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads) }
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') }
@@ -424,6 +424,95 @@ describe Group do
end
end
+ describe '#direct_and_indirect_members', :nested_groups do
+ let!(:group) { create(:group, :nested) }
+ let!(:sub_group) { create(:group, parent: group) }
+ let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
+ let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
+ let!(:other_developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
+
+ it 'returns parents members' do
+ expect(group.direct_and_indirect_members).to include(developer)
+ expect(group.direct_and_indirect_members).to include(master)
+ end
+
+ it 'returns descendant members' do
+ expect(group.direct_and_indirect_members).to include(other_developer)
+ end
+ end
+
+ describe '#users_with_descendants', :nested_groups do
+ let(:user_a) { create(:user) }
+ let(:user_b) { create(:user) }
+
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+
+ it 'returns member users on every nest level without duplication' do
+ group.add_developer(user_a)
+ nested_group.add_developer(user_b)
+ deep_nested_group.add_developer(user_a)
+
+ expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
+ expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
+ expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a)
+ end
+ end
+
+ describe '#direct_and_indirect_users', :nested_groups do
+ let(:user_a) { create(:user) }
+ let(:user_b) { create(:user) }
+ let(:user_c) { create(:user) }
+ let(:user_d) { create(:user) }
+
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:project) { create(:project, namespace: group) }
+
+ before do
+ group.add_developer(user_a)
+ group.add_developer(user_c)
+ nested_group.add_developer(user_b)
+ deep_nested_group.add_developer(user_a)
+ project.add_developer(user_d)
+ end
+
+ it 'returns member users on every nest level without duplication' do
+ expect(group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c, user_d)
+ expect(nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
+ expect(deep_nested_group.direct_and_indirect_users).to contain_exactly(user_a, user_b, user_c)
+ end
+
+ it 'does not return members of projects belonging to ancestor groups' do
+ expect(nested_group.direct_and_indirect_users).not_to include(user_d)
+ end
+ end
+
+ describe '#project_users_with_descendants', :nested_groups do
+ let(:user_a) { create(:user) }
+ let(:user_b) { create(:user) }
+ let(:user_c) { create(:user) }
+
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:project_a) { create(:project, namespace: group) }
+ let(:project_b) { create(:project, namespace: nested_group) }
+ let(:project_c) { create(:project, namespace: deep_nested_group) }
+
+ it 'returns members of all projects in group and subgroups' do
+ project_a.add_developer(user_a)
+ project_b.add_developer(user_b)
+ project_c.add_developer(user_c)
+
+ expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c)
+ expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c)
+ expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c)
+ end
+ end
+
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
master = create(:user)
@@ -602,4 +691,12 @@ describe Group do
end
end
end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', true do
+ let(:model_object) { create(:group, :with_avatar) }
+ let(:upload_attribute) { :avatar }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
index 2afdd6751a4..fc30f3056e5 100644
--- a/spec/models/guest_spec.rb
+++ b/spec/models/guest_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe Guest do
- let(:public_project) { build_stubbed(:project, :public) }
- let(:private_project) { build_stubbed(:project, :private) }
- let(:internal_project) { build_stubbed(:project, :internal) }
+ set(:public_project) { create(:project, :public) }
+ set(:private_project) { create(:project, :private) }
+ set(:internal_project) { create(:project, :internal) }
describe '.can_pull?' do
context 'when project is private' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 11154291368..128acf83686 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -376,6 +376,48 @@ describe Issue do
end
end
+ describe '#suggested_branch_name' do
+ let(:repository) { double }
+
+ subject { build(:issue) }
+
+ before do
+ allow(subject.project).to receive(:repository).and_return(repository)
+ end
+
+ context '#to_branch_name does not exists' do
+ before do
+ allow(repository).to receive(:branch_exists?).and_return(false)
+ end
+
+ it 'returns #to_branch_name' do
+ expect(subject.suggested_branch_name).to eq(subject.to_branch_name)
+ end
+ end
+
+ context '#to_branch_name exists not ending with -index' do
+ before do
+ allow(repository).to receive(:branch_exists?).and_return(true)
+ allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\d/).and_return(false)
+ end
+
+ it 'returns #to_branch_name ending with -2' do
+ expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-2")
+ end
+ end
+
+ context '#to_branch_name exists ending with -index' do
+ before do
+ allow(repository).to receive(:branch_exists?).and_return(true)
+ allow(repository).to receive(:branch_exists?).with("#{subject.to_branch_name}-3").and_return(false)
+ end
+
+ it 'returns #to_branch_name ending with max index + 1' do
+ expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
+ end
+ end
+ end
+
describe '#has_related_branch?' do
let(:issue) { create(:issue, title: "Blue Bell Knoll") }
subject { issue.has_related_branch? }
@@ -425,6 +467,27 @@ describe Issue do
end
end
+ describe '#can_be_worked_on?' do
+ let(:project) { build(:project) }
+ subject { build(:issue, :opened, project: project) }
+
+ context 'is closed' do
+ subject { build(:issue, :closed) }
+
+ it { is_expected.not_to be_can_be_worked_on }
+ end
+
+ context 'project is forked' do
+ before do
+ allow(project).to receive(:forked?).and_return(true)
+ end
+
+ it { is_expected.not_to be_can_be_worked_on }
+ end
+
+ it { is_expected.to be_can_be_worked_on }
+ end
+
describe '#participants' do
context 'using a public project' do
let(:project) { create(:project, :public) }
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index a182116d637..6e35511e848 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -62,9 +62,7 @@ describe LfsObject do
.with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
.once
- lfs_object = create(:lfs_object)
- lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
- lfs_object.save!
+ create(:lfs_object, :with_file)
end
end
end
@@ -81,5 +79,44 @@ describe LfsObject do
end
end
end
+
+ 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)
+ expect(lfs_object.file).to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ stub_lfs_object_storage(direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ it 'is stored remotely' do
+ expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(lfs_object.file).not_to be_file_storage
+ expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 5a3b5b1f517..ffc78015f94 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -28,52 +28,12 @@ describe GroupMember do
end
end
- describe 'notifications' do
- describe "#after_create" do
- it "sends email to user" do
- membership = build(:group_member)
+ it_behaves_like 'members notifications', :group
- allow(membership).to receive(:notification_service)
- .and_return(double('NotificationService').as_null_object)
- expect(membership).to receive(:notification_service)
+ describe '#real_source_type' do
+ subject { create(:group_member).real_source_type }
- membership.save
- end
- end
-
- describe "#after_update" do
- before do
- @group_member = create :group_member
- allow(@group_member).to receive(:notification_service)
- .and_return(double('NotificationService').as_null_object)
- end
-
- it "sends email to user" do
- expect(@group_member).to receive(:notification_service)
- @group_member.update_attribute(:access_level, GroupMember::MASTER)
- end
-
- it "does not send an email when the access level has not changed" do
- expect(@group_member).not_to receive(:notification_service)
- @group_member.update_attribute(:access_level, GroupMember::OWNER)
- end
- end
-
- describe '#after_accept_request' do
- it 'calls NotificationService.accept_group_access_request' do
- member = create(:group_member, user: build(:user), requested_at: Time.now)
-
- expect_any_instance_of(NotificationService).to receive(:new_group_member)
-
- member.__send__(:after_accept_request)
- end
- end
-
- describe '#real_source_type' do
- subject { create(:group_member).real_source_type }
-
- it { is_expected.to eq 'Group' }
- end
+ it { is_expected.to eq 'Group' }
end
describe '#update_two_factor_requirement' do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index b8b0e63f92e..574eb468e4c 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -123,15 +123,5 @@ describe ProjectMember do
it { expect(@project_2.users).to be_empty }
end
- describe 'notifications' do
- describe '#after_accept_request' do
- it 'calls NotificationService.new_project_member' do
- member = create(:project_member, user: create(:user), requested_at: Time.now)
-
- expect_any_instance_of(NotificationService).to receive(:new_project_member)
-
- member.__send__(:after_accept_request)
- end
- end
- end
+ it_behaves_like 'members notifications', :project
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f73f44ca0ad..92e33a64d26 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -17,11 +17,17 @@ describe MergeRequest do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(NonatomicInternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:merge_request) }
+ let(:scope_attrs) { { project: instance.target_project } }
+ let(:usage) { :merge_requests }
+ end
end
describe 'validation' do
@@ -1063,6 +1069,22 @@ describe MergeRequest do
end
end
+ describe '#short_merge_commit_sha' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ it 'returns short id when there is a merge_commit_sha' do
+ merge_request.merge_commit_sha = 'f7ce827c314c9340b075657fd61c789fb01cf74d'
+
+ expect(merge_request.short_merge_commit_sha).to eq('f7ce827c')
+ end
+
+ it 'returns nil when there is no merge_commit_sha' do
+ merge_request.merge_commit_sha = nil
+
+ expect(merge_request.short_merge_commit_sha).to be_nil
+ end
+ end
+
describe '#can_be_reverted?' do
context 'when there is no merge_commit for the MR' do
before do
@@ -1207,7 +1229,7 @@ describe MergeRequest do
it 'enqueues MergeWorker job and updates merge_jid' do
merge_request = create(:merge_request)
user_id = double(:user_id)
- params = double(:params)
+ params = {}
merge_jid = 'hash-123'
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
@@ -1223,38 +1245,50 @@ describe MergeRequest do
describe '#check_if_can_be_merged' do
let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
- subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
+ shared_examples 'checking if can be merged' do
+ context 'when it is not broken and has no conflicts' do
+ before do
+ allow(subject).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?).and_return(true)
+ end
- context 'when it is not broken and has no conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(true)
+ it 'is marked as mergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
+ end
end
- it 'is marked as mergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
- end
- end
+ context 'when broken' do
+ before do
+ allow(subject).to receive(:broken?) { true }
+ end
- context 'when broken' do
- before do
- allow(subject).to receive(:broken?) { true }
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
end
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ context 'when it has conflicts' do
+ before do
+ allow(subject).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?).and_return(false)
+ end
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
end
end
- context 'when it has conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
+ context 'when merge_status is unchecked' do
+ subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
+ it_behaves_like 'checking if can be merged'
+ end
+
+ context 'when merge_status is unchecked' do
+ subject { create(:merge_request, source_project: project, merge_status: :cannot_be_merged_recheck) }
+
+ it_behaves_like 'checking if can be merged'
end
end
@@ -2042,6 +2076,53 @@ describe MergeRequest do
expect(subject.merge_jid).to be_nil
end
end
+
+ describe 'transition to cannot_be_merged' do
+ let(:notification_service) { double(:notification_service) }
+ let(:todo_service) { double(:todo_service) }
+
+ subject { create(:merge_request, merge_status: :unchecked) }
+
+ before do
+ allow(NotificationService).to receive(:new).and_return(notification_service)
+ allow(TodoService).to receive(:new).and_return(todo_service)
+ end
+
+ it 'notifies, but does not notify again if rechecking still results in cannot_be_merged' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
+
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+
+ it 'notifies whenever merge request is newly unmergeable' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
+
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_mergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+ end
+
+ describe 'check_state?' do
+ it 'indicates whether MR is still checking for mergeability' do
+ state_machine = described_class.state_machines[:merge_status]
+ check_states = [:unchecked, :cannot_be_merged_recheck]
+
+ check_states.each do |merge_status|
+ expect(state_machine.check_state?(merge_status)).to be true
+ end
+
+ (state_machine.states.map(&:name) - check_states).each do |merge_status|
+ expect(state_machine.check_state?(merge_status)).to be false
+ end
+ end
+ end
end
describe '#should_be_rebased?' do
@@ -2173,4 +2254,39 @@ describe MergeRequest do
expect(merge_request.can_allow_maintainer_to_push?(user)).to be_truthy
end
end
+
+ describe '#merge_participants' do
+ it 'contains author' do
+ expect(subject.merge_participants).to eq([subject.author])
+ end
+
+ describe 'when merge_when_pipeline_succeeds? is true' do
+ describe 'when merge user is author' do
+ let(:user) { create(:user) }
+ subject do
+ create(:merge_request,
+ merge_when_pipeline_succeeds: true,
+ merge_user: user,
+ author: user)
+ end
+
+ it 'contains author only' do
+ expect(subject.merge_participants).to eq([subject.author])
+ end
+ end
+
+ describe 'when merge user and author are different users' do
+ let(:merge_user) { create(:user) }
+ subject do
+ create(:merge_request,
+ merge_when_pipeline_succeeds: true,
+ merge_user: merge_user)
+ end
+
+ it 'contains author and merge user' do
+ expect(subject.merge_participants).to eq([subject.author, merge_user])
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 47f4a792e5c..4bb9717d33e 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -1,6 +1,26 @@
require 'spec_helper'
describe Milestone do
+ describe 'modules' do
+ context 'with a project' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:milestone, project: build(:project), group: nil) }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :milestones }
+ end
+ end
+
+ context 'with a group' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:milestone, project: nil, group: build(:group)) }
+ let(:scope_attrs) { { namespace: instance.group } }
+ let(:usage) { :milestones }
+ end
+ end
+ end
+
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
@@ -96,7 +116,9 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
end
- it { expect(milestone.expired?).to be_truthy }
+ it 'returns true when due_date is in the past' do
+ expect(milestone.expired?).to be_truthy
+ end
end
context "not expired" do
@@ -104,17 +126,19 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
end
- it { expect(milestone.expired?).to be_falsey }
+ it 'returns false when due_date is in the future' do
+ expect(milestone.expired?).to be_falsey
+ end
end
end
describe '#upcoming?' do
- it 'returns true' do
+ it 'returns true when start_date is in the future' do
milestone = build(:milestone, start_date: Time.now + 1.month)
expect(milestone.upcoming?).to be_truthy
end
- it 'returns false' do
+ it 'returns false when start_date is in the past' do
milestone = build(:milestone, start_date: Date.today.prev_year)
expect(milestone.upcoming?).to be_falsey
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 62e95a622eb..6f702d8d95e 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -5,6 +5,7 @@ describe Namespace do
let!(:namespace) { create(:namespace) }
let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:repository_storage) { 'default' }
describe 'associations' do
it { is_expected.to have_many :projects }
@@ -201,7 +202,7 @@ describe Namespace do
it "moves dir if path changed" do
namespace.update_attributes(path: namespace.full_path + '_new')
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{namespace.path}/#{project.path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
context 'with subgroups', :nested_groups do
@@ -281,7 +282,7 @@ describe Namespace do
namespace.update_attributes(path: namespace.full_path + '_new')
expect(before_disk_path).to eq(project.disk_path)
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
end
end
@@ -322,7 +323,7 @@ describe Namespace do
end
it 'schedules the namespace for deletion' do
- expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
namespace.destroy
end
@@ -344,7 +345,7 @@ describe Namespace do
end
it 'schedules the namespace for deletion' do
- expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage, deleted_path)
child.destroy
end
@@ -398,6 +399,21 @@ describe Namespace do
end
end
+ describe '#self_and_hierarchy', :nested_groups do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct tree' do
+ expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
describe '#ancestors', :nested_groups do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
diff --git a/spec/models/note_diff_file_spec.rb b/spec/models/note_diff_file_spec.rb
new file mode 100644
index 00000000000..591c1a89748
--- /dev/null
+++ b/spec/models/note_diff_file_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+describe NoteDiffFile do
+ describe 'associations' do
+ it { is_expected.to belong_to(:diff_note) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:diff_note) }
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 86962cd8d61..6a6c71e6c82 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -91,6 +91,23 @@ describe Note do
it "keeps the commit around" do
expect(note.project.repository.kept_around?(commit.id)).to be_truthy
end
+
+ it 'does not generate N+1 queries for participants', :request_store do
+ def retrieve_participants
+ commit.notes_with_associations.map(&:participants).to_a
+ end
+
+ # Project authorization checks are cached, establish a baseline
+ retrieve_participants
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ retrieve_participants
+ end
+
+ create(:note_on_commit, project: note.project, note: 'another note', noteable_id: commit.id)
+
+ expect { retrieve_participants }.not_to exceed_query_limit(control_count)
+ end
end
describe 'authorization' do
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 2a0d102d3fe..12681a147b4 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -40,7 +40,12 @@ RSpec.describe NotificationSetting do
expect(notification_setting.new_issue).to eq(true)
expect(notification_setting.close_issue).to eq(true)
expect(notification_setting.merge_merge_request).to eq(true)
- expect(notification_setting.close_merge_request).to eq(false)
+
+ # In Rails 5 assigning a value which is not explicitly `true` or `false` ("nil" in this case)
+ # to a boolean column transforms it to `true`.
+ # In Rails 4 it transforms the value to `false` with deprecation warning.
+ # Replace `eq(Gitlab.rails5?)` with `eq(true)` when removing rails5? code.
+ expect(notification_setting.close_merge_request).to eq(Gitlab.rails5?)
expect(notification_setting.reopen_merge_request).to eq(false)
end
end
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
new file mode 100644
index 00000000000..4aa62028169
--- /dev/null
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectCiCdSetting do
+ describe '.available?' do
+ before do
+ described_class.reset_column_information
+ end
+
+ it 'returns true' do
+ expect(described_class).to be_available
+ end
+
+ it 'memoizes the schema version' do
+ expect(ActiveRecord::Migrator)
+ .to receive(:current_version)
+ .and_call_original
+ .once
+
+ 2.times { described_class.available? }
+ end
+ end
+end
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
new file mode 100644
index 00000000000..f7033b28c76
--- /dev/null
+++ b/spec/models/project_import_state_spec.rb
@@ -0,0 +1,13 @@
+require 'rails_helper'
+
+describe ProjectImportState, type: :model do
+ subject { create(:import_state) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+end
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 4c61bc0af95..6e417323e40 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -26,24 +26,49 @@ describe GemnasiumService do
end
end
+ describe "deprecated?" do
+ let(:project) { create(:project, :repository) }
+ let(:gemnasium_service) { described_class.new }
+
+ before do
+ allow(gemnasium_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ token: 'verySecret',
+ api_key: 'GemnasiumUserApiKey'
+ )
+ end
+
+ it "is true" do
+ expect(gemnasium_service.deprecated?).to be true
+ end
+
+ it "can't create a new service" do
+ expect(gemnasium_service.save).to be false
+ expect(gemnasium_service.errors[:base].first)
+ .to eq('Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available.')
+ end
+ end
+
describe "Execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
+ let(:gemnasium_service) { described_class.new }
+ let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
before do
- @gemnasium_service = described_class.new
- allow(@gemnasium_service).to receive_messages(
+ allow(gemnasium_service).to receive_messages(
project_id: project.id,
project: project,
service_hook: true,
token: 'verySecret',
api_key: 'GemnasiumUserApiKey'
)
- @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
end
it "calls Gemnasium service" do
expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
- @gemnasium_service.execute(@sample_data)
+ gemnasium_service.execute(sample_data)
end
end
end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 733086e258f..8d9ee96227f 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -30,7 +30,7 @@ describe MicrosoftTeamsService do
describe "#execute" do
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository, :wiki_repo) }
before do
allow(chat_service).to receive_messages(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2675c2f52c1..af2240f4f89 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -63,7 +63,6 @@ describe Project do
it { is_expected.to have_many(:build_trace_section_names)}
it { is_expected.to have_many(:runner_projects) }
it { is_expected.to have_many(:runners) }
- it { is_expected.to have_many(:active_runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:pages_domains) }
@@ -77,7 +76,7 @@ describe Project do
it { is_expected.to have_many(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:delete_all) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) }
@@ -93,6 +92,23 @@ describe Project do
end
end
+ context 'when creating a new project' do
+ it 'automatically creates a CI/CD settings row' do
+ project = create(:project)
+
+ expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
+ expect(project.ci_cd_settings).to be_persisted
+ end
+ end
+
+ context 'updating cd_cd_settings' do
+ it 'does not raise an error' do
+ project = create(:project)
+
+ expect { project.update(ci_cd_settings: nil) }.not_to raise_exception
+ end
+ end
+
describe '#members & #requesters' do
let(:project) { create(:project, :public, :access_requestable) }
let(:requester) { create(:user) }
@@ -293,12 +309,12 @@ describe Project do
describe 'project token' do
it 'sets an random token if none provided' do
- project = FactoryBot.create :project, runners_token: ''
+ project = FactoryBot.create(:project, runners_token: '')
expect(project.runners_token).not_to eq('')
end
it 'does not set an random token if one provided' do
- project = FactoryBot.create :project, runners_token: 'my-token'
+ project = FactoryBot.create(:project, runners_token: 'my-token')
expect(project.runners_token).to eq('my-token')
end
end
@@ -325,7 +341,7 @@ describe Project do
let(:owner) { create(:user, name: 'Gitlab') }
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
let(:project) { create(:project, path: 'sample-project', namespace: namespace) }
- let(:group) { create(:group, name: 'Group', path: 'sample-group', owner: owner) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group') }
context 'when nil argument' do
it 'returns nil' do
@@ -440,14 +456,6 @@ describe Project do
end
end
- describe '#repository_storage_path' do
- let(:project) { create(:project) }
-
- it 'returns the repository storage path' do
- expect(Dir.exist?(project.repository_storage_path)).to be(true)
- end
- end
-
it 'returns valid url to repo' do
project = described_class.new(path: 'somewhere')
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
@@ -461,6 +469,34 @@ describe Project do
end
end
+ describe "#readme_url" do
+ let(:project) { create(:project, :repository, path: "somewhere") }
+
+ context 'with a non-existing repository' do
+ it 'returns nil' do
+ allow(project.repository).to receive(:tree).with(:head).and_return(nil)
+
+ expect(project.readme_url).to be_nil
+ end
+ end
+
+ context 'with an existing repository' do
+ context 'when no README exists' do
+ it 'returns nil' do
+ allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
+
+ expect(project.readme_url).to be_nil
+ end
+ end
+
+ context 'when a README exists' do
+ it 'returns the README' do
+ expect(project.readme_url).to eql("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere/blob/master/README.md")
+ end
+ end
+ end
+ end
+
describe "#new_issuable_address" do
let(:project) { create(:project, path: "somewhere") }
let(:user) { create(:user) }
@@ -631,7 +667,7 @@ describe Project do
describe '#to_param' do
context 'with namespace' do
before do
- @group = create :group, name: 'gitlab'
+ @group = create(:group, name: 'gitlab')
@project = create(:project, name: 'gitlabhq', namespace: @group)
end
@@ -858,8 +894,8 @@ describe Project do
describe '#star_count' do
it 'counts stars from multiple users' do
- user1 = create :user
- user2 = create :user
+ user1 = create(:user)
+ user2 = create(:user)
project = create(:project, :public)
expect(project.star_count).to eq(0)
@@ -881,7 +917,7 @@ describe Project do
end
it 'counts stars on the right project' do
- user = create :user
+ user = create(:user)
project1 = create(:project, :public)
project2 = create(:project, :public)
@@ -1099,7 +1135,7 @@ describe Project do
end
context 'repository storage by default' do
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
before do
storages = {
@@ -1138,45 +1174,106 @@ describe Project do
end
end
- describe '#any_runners' do
- let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
- let(:specific_runner) { create(:ci_runner) }
- let(:shared_runner) { create(:ci_runner, :shared) }
+ describe '#any_runners?' do
+ context 'shared runners' do
+ let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
+ let(:specific_runner) { create(:ci_runner) }
+ let(:shared_runner) { create(:ci_runner, :shared) }
- context 'for shared runners disabled' do
- let(:shared_runners_enabled) { false }
+ context 'for shared runners disabled' do
+ let(:shared_runners_enabled) { false }
- it 'has no runners available' do
- expect(project.any_runners?).to be_falsey
- end
+ it 'has no runners available' do
+ expect(project.any_runners?).to be_falsey
+ end
- it 'has a specific runner' do
- project.runners << specific_runner
- expect(project.any_runners?).to be_truthy
- end
+ it 'has a specific runner' do
+ project.runners << specific_runner
+
+ expect(project.any_runners?).to be_truthy
+ end
- it 'has a shared runner, but they are prohibited to use' do
- shared_runner
- expect(project.any_runners?).to be_falsey
+ it 'has a shared runner, but they are prohibited to use' do
+ shared_runner
+
+ expect(project.any_runners?).to be_falsey
+ end
+
+ it 'checks the presence of specific runner' do
+ project.runners << specific_runner
+
+ expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
+ end
+
+ it 'returns false if match cannot be found' do
+ project.runners << specific_runner
+
+ expect(project.any_runners? { false }).to be_falsey
+ end
end
- it 'checks the presence of specific runner' do
- project.runners << specific_runner
- expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
+ context 'for shared runners enabled' do
+ let(:shared_runners_enabled) { true }
+
+ it 'has a shared runner' do
+ shared_runner
+
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it 'checks the presence of shared runner' do
+ shared_runner
+
+ expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
+ end
+
+ it 'returns false if match cannot be found' do
+ shared_runner
+
+ expect(project.any_runners? { false }).to be_falsey
+ end
end
end
- context 'for shared runners enabled' do
- let(:shared_runners_enabled) { true }
+ context 'group runners' do
+ let(:project) { create(:project, group_runners_enabled: group_runners_enabled) }
+ let(:group) { create(:group, projects: [project]) }
+ let(:group_runner) { create(:ci_runner, groups: [group]) }
+
+ context 'for group runners disabled' do
+ let(:group_runners_enabled) { false }
+
+ it 'has no runners available' do
+ expect(project.any_runners?).to be_falsey
+ end
+
+ it 'has a group runner, but they are prohibited to use' do
+ group_runner
- it 'has a shared runner' do
- shared_runner
- expect(project.any_runners?).to be_truthy
+ expect(project.any_runners?).to be_falsey
+ end
end
- it 'checks the presence of shared runner' do
- shared_runner
- expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
+ context 'for group runners enabled' do
+ let(:group_runners_enabled) { true }
+
+ it 'has a group runner' do
+ group_runner
+
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it 'checks the presence of group runner' do
+ group_runner
+
+ expect(project.any_runners? { |runner| runner == group_runner }).to be_truthy
+ end
+
+ it 'returns false if match cannot be found' do
+ group_runner
+
+ expect(project.any_runners? { false }).to be_falsey
+ end
end
end
end
@@ -1223,7 +1320,7 @@ describe Project do
end
describe '#pages_deployed?' do
- let(:project) { create :project }
+ let(:project) { create(:project) }
subject { project.pages_deployed? }
@@ -1241,8 +1338,8 @@ describe Project do
end
describe '#pages_url' do
- let(:group) { create :group, name: group_name }
- let(:project) { create :project, namespace: group, name: project_name }
+ let(:group) { create(:group, name: group_name) }
+ let(:project) { create(:project, namespace: group, name: project_name) }
let(:domain) { 'Example.com' }
subject { project.pages_url }
@@ -1268,8 +1365,8 @@ describe Project do
end
describe '#pages_group_url' do
- let(:group) { create :group, name: group_name }
- let(:project) { create :project, namespace: group, name: project_name }
+ let(:group) { create(:group, name: group_name) }
+ let(:project) { create(:project, namespace: group, name: project_name) }
let(:domain) { 'Example.com' }
let(:port) { 1234 }
@@ -1386,8 +1483,8 @@ describe Project do
let(:private_group) { create(:group, visibility_level: 0) }
let(:internal_group) { create(:group, visibility_level: 10) }
- let(:private_project) { create :project, :private, group: private_group }
- let(:internal_project) { create :project, :internal, group: internal_group }
+ let(:private_project) { create(:project, :private, group: private_group) }
+ let(:internal_project) { create(:project, :internal, group: internal_group) }
context 'when group is private project can not be internal' do
it { expect(private_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_falsey }
@@ -1452,7 +1549,7 @@ describe Project do
.and_return(false)
allow(shell).to receive(:create_repository)
- .with(project.repository_storage_path, project.disk_path)
+ .with(project.repository_storage, project.disk_path)
.and_return(true)
expect(project).to receive(:create_repository).with(force: true)
@@ -1483,52 +1580,6 @@ describe Project do
end
end
- describe '#user_can_push_to_empty_repo?' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- it 'returns false when default_branch_protection is in full protection and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
-
- it 'returns false when default_branch_protection only lets devs merge and user is dev' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
-
- it 'returns true when default_branch_protection lets devs push and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns true when default_branch_protection is unprotected and user is developer' do
- project.add_developer(user)
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns true when user is master' do
- project.add_master(user)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
- end
-
- it 'returns false when the repo is not empty' do
- project.add_master(user)
- expect(project).to receive(:empty_repo?).and_return(false)
-
- expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
- end
- end
-
describe '#container_registry_url' do
let(:project) { create(:project) }
@@ -1680,7 +1731,8 @@ describe Project do
it 'resets project import_error' do
error_message = 'Some error'
- mirror = create(:project_empty_repo, :import_started, import_error: error_message)
+ mirror = create(:project_empty_repo, :import_started)
+ mirror.import_state.update_attributes(last_error: error_message)
expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil)
end
@@ -1828,6 +1880,83 @@ describe Project do
it { expect(project.gitea_import?).to be true }
end
+ describe '#has_remote_mirror?' do
+ let(:project) { create(:project, :remote_mirror, :import_started) }
+ subject { project.has_remote_mirror? }
+
+ before do
+ allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
+ end
+
+ it 'returns true when a remote mirror is enabled' do
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when remote mirror is disabled' do
+ project.remote_mirrors.first.update_attributes(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ describe '#update_remote_mirrors' do
+ let(:project) { create(:project, :remote_mirror, :import_started) }
+ delegate :update_remote_mirrors, to: :project
+
+ before do
+ allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
+ end
+
+ it 'syncs enabled remote mirror' do
+ expect_any_instance_of(RemoteMirror).to receive(:sync)
+
+ update_remote_mirrors
+ end
+
+ it 'does nothing when remote mirror is disabled globally and not overridden' do
+ stub_application_setting(mirror_available: false)
+ project.remote_mirror_available_overridden = false
+
+ expect_any_instance_of(RemoteMirror).not_to receive(:sync)
+
+ update_remote_mirrors
+ end
+
+ it 'does not sync disabled remote mirrors' do
+ project.remote_mirrors.first.update_attributes(enabled: false)
+
+ expect_any_instance_of(RemoteMirror).not_to receive(:sync)
+
+ update_remote_mirrors
+ end
+ end
+
+ describe '#remote_mirror_available?' do
+ let(:project) { create(:project) }
+
+ context 'when remote mirror global setting is enabled' do
+ it 'returns true' do
+ expect(project.remote_mirror_available?).to be(true)
+ end
+ end
+
+ context 'when remote mirror global setting is disabled' do
+ before do
+ stub_application_setting(mirror_available: false)
+ end
+
+ it 'returns true when overridden' do
+ project.remote_mirror_available_overridden = true
+
+ expect(project.remote_mirror_available?).to be(true)
+ end
+
+ it 'returns false when not overridden' do
+ expect(project.remote_mirror_available?).to be(false)
+ end
+ end
+ end
+
describe '#ancestors_upto', :nested_groups do
let(:parent) { create(:group) }
let(:child) { create(:group, parent: parent) }
@@ -2349,8 +2478,8 @@ describe Project do
end
describe '#pages_url' do
- let(:group) { create :group, name: 'Group' }
- let(:nested_group) { create :group, parent: group }
+ let(:group) { create(:group, name: 'Group') }
+ let(:nested_group) { create(:group, parent: group) }
let(:domain) { 'Example.com' }
subject { project.pages_url }
@@ -2361,7 +2490,7 @@ describe Project do
end
context 'top-level group' do
- let(:project) { create :project, namespace: group, name: project_name }
+ let(:project) { create(:project, namespace: group, name: project_name) }
context 'group page' do
let(:project_name) { 'group.example.com' }
@@ -2377,7 +2506,7 @@ describe Project do
end
context 'nested group' do
- let(:project) { create :project, namespace: nested_group, name: project_name }
+ let(:project) { create(:project, namespace: nested_group, name: project_name) }
let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
context 'group page' do
@@ -2395,7 +2524,7 @@ describe Project do
end
describe '#http_url_to_repo' do
- let(:project) { create :project }
+ let(:project) { create(:project) }
it 'returns the url to the repo without a username' do
expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
@@ -2673,7 +2802,7 @@ describe Project do
describe '#ensure_storage_path_exists' do
it 'delegates to gitlab_shell to ensure namespace is created' do
- expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, project.base_dir)
+ expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage, project.base_dir)
project.ensure_storage_path_exists
end
@@ -2712,12 +2841,12 @@ describe Project do
expect(gitlab_shell).to receive(:mv_repository)
.ordered
- .with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}")
+ .with(project.repository_storage, "#{project.namespace.full_path}/foo", "#{project.full_path}")
.and_return(true)
expect(gitlab_shell).to receive(:mv_repository)
.ordered
- .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
+ .with(project.repository_storage, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
.and_return(true)
expect_any_instance_of(SystemHooksService)
@@ -2866,7 +2995,7 @@ describe Project do
it 'delegates to gitlab_shell to ensure namespace is created' do
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
- expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, hashed_prefix)
+ expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage, hashed_prefix)
project.ensure_storage_path_exists
end
@@ -3324,7 +3453,8 @@ describe Project do
context 'with an import JID' do
it 'unsets the import JID' do
- project = create(:project, import_jid: '123')
+ project = create(:project)
+ create(:import_state, project: project, jid: '123')
expect(Gitlab::SidekiqStatus)
.to receive(:unset)
@@ -3585,4 +3715,64 @@ describe Project do
it { is_expected.not_to be_valid }
end
end
+
+ describe '#toggle_ci_cd_settings!' do
+ it 'toggles the value on #settings' do
+ project = create(:project, group_runners_enabled: false)
+
+ expect(project.group_runners_enabled).to be false
+
+ project.toggle_ci_cd_settings!(:group_runners_enabled)
+
+ expect(project.group_runners_enabled).to be true
+ end
+ end
+
+ describe '#gitlab_deploy_token' do
+ let(:project) { create(:project) }
+
+ subject { project.gitlab_deploy_token }
+
+ context 'when there is a gitlab deploy token associated' do
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
+
+ it { is_expected.to eq(deploy_token) }
+ end
+
+ context 'when there is no a gitlab deploy token associated' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a gitlab deploy token associated but is has been revoked' do
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :revoked, projects: [project]) }
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a gitlab deploy token associated but it is expired' do
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a deploy token associated with a different name' do
+ let!(:deploy_token) { create(:deploy_token, projects: [project]) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when there is a deploy token associated to a different project' do
+ let(:project_2) { create(:project) }
+ let!(:deploy_token) { create(:deploy_token, projects: [project_2]) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', true do
+ let(:model_object) { create(:project, :with_avatar) }
+ let(:upload_attribute) { :avatar }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 5cff2af4aca..38a3590ad12 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -4,26 +4,6 @@ describe ProjectStatistics do
let(:project) { create :project }
let(:statistics) { project.statistics }
- describe 'constants' do
- describe 'STORAGE_COLUMNS' do
- it 'is an array of symbols' do
- expect(described_class::STORAGE_COLUMNS).to be_kind_of Array
- expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol]
- end
- end
-
- describe 'STATISTICS_COLUMNS' do
- it 'is an array of symbols' do
- expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array
- expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol]
- end
-
- it 'includes all storage columns' do
- expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS
- end
- end
- end
-
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:namespace) }
@@ -63,7 +43,6 @@ describe ProjectStatistics do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
allow(statistics).to receive(:update_lfs_objects_size)
- allow(statistics).to receive(:update_build_artifacts_size)
allow(statistics).to receive(:update_storage_size)
end
@@ -76,7 +55,6 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
expect(statistics).to have_received(:update_lfs_objects_size)
- expect(statistics).to have_received(:update_build_artifacts_size)
end
end
@@ -89,7 +67,6 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).not_to have_received(:update_commit_count)
expect(statistics).not_to have_received(:update_repository_size)
- expect(statistics).not_to have_received(:update_build_artifacts_size)
end
end
end
@@ -131,40 +108,6 @@ describe ProjectStatistics do
end
end
- describe '#update_build_artifacts_size' do
- let!(:pipeline) { create(:ci_pipeline, project: project) }
-
- context 'when new job artifacts are calculated' do
- let(:ci_build) { create(:ci_build, pipeline: pipeline) }
-
- before do
- create(:ci_job_artifact, :archive, project: pipeline.project, job: ci_build)
- end
-
- it "stores the size of related build artifacts" do
- statistics.update_build_artifacts_size
-
- expect(statistics.build_artifacts_size).to be(106365)
- end
-
- it 'calculates related build artifacts by project' do
- expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 }
-
- statistics.update_build_artifacts_size
- end
- end
-
- context 'when legacy artifacts are used' do
- let!(:ci_build) { create(:ci_build, pipeline: pipeline, artifacts_size: 10.megabytes) }
-
- it "stores the size of related build artifacts" do
- statistics.update_build_artifacts_size
-
- expect(statistics.build_artifacts_size).to eq(10.megabytes)
- end
- end
- end
-
describe '#update_storage_size' do
it "sums all storage counters" do
statistics.update!(
@@ -177,4 +120,27 @@ describe ProjectStatistics do
expect(statistics.storage_size).to eq 5
end
end
+
+ describe '.increment_statistic' do
+ it 'increases the statistic by that amount' do
+ expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) }
+ .to change { statistics.reload.build_artifacts_size }
+ .by(13)
+ end
+
+ context 'when the amount is 0' do
+ it 'does not execute a query' do
+ project
+ expect { described_class.increment_statistic(project.id, :build_artifacts_size, 0) }
+ .not_to exceed_query_limit(0)
+ end
+ end
+
+ context 'when using an invalid column' do
+ it 'raises an error' do
+ expect { described_class.increment_statistic(project.id, :id, 13) }
+ .to raise_error(ArgumentError, "Cannot increment attribute: id")
+ end
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 374a157bec0..f1142832f1a 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe ProjectWiki do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
let(:repository) { project.repository }
let(:user) { project.owner }
let(:gitlab_shell) { Gitlab::Shell.new }
@@ -11,7 +11,7 @@ describe ProjectWiki do
subject { project_wiki }
it { is_expected.to delegate_method(:empty?).to :pages }
- it { is_expected.to delegate_method(:repository_storage_path).to :project }
+ it { is_expected.to delegate_method(:repository_storage).to :project }
it { is_expected.to delegate_method(:hashed_storage?).to :project }
describe "#full_path" do
@@ -159,6 +159,17 @@ describe ProjectWiki do
expect(page.title).to eq("autre pagé")
end
end
+
+ context 'pages with invalidly-encoded content' do
+ before do
+ create_page("encoding is fun", "f\xFCr".b)
+ end
+
+ it "can find the page" do
+ page = subject.find_page("encoding is fun")
+ expect(page.content).to eq("fr")
+ end
+ end
end
context 'when Gitaly wiki_find_page is enabled' do
@@ -328,6 +339,8 @@ describe ProjectWiki do
end
describe '#create_repo!' do
+ let(:project) { create(:project) }
+
it 'creates a repository' do
expect(raw_repository.exists?).to eq(false)
expect(subject.repository).to receive(:after_create)
@@ -339,6 +352,8 @@ describe ProjectWiki do
end
describe '#ensure_repository' do
+ let(:project) { create(:project) }
+
it 'creates the repository if it not exist' do
expect(raw_repository.exists?).to eq(false)
@@ -377,7 +392,7 @@ describe ProjectWiki do
end
def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
new file mode 100644
index 00000000000..a80800c6c92
--- /dev/null
+++ b/spec/models/remote_mirror_spec.rb
@@ -0,0 +1,267 @@
+require 'rails_helper'
+
+describe RemoteMirror do
+ describe 'URL validation' do
+ context 'with a valid URL' do
+ it 'should be valid' do
+ remote_mirror = build(:remote_mirror)
+ expect(remote_mirror).to be_valid
+ end
+ end
+
+ context 'with an invalid URL' do
+ it 'should not be valid' do
+ remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid')
+ expect(remote_mirror).not_to be_valid
+ expect(remote_mirror.errors[:url].size).to eq(2)
+ end
+ end
+ end
+
+ describe 'encrypting credentials' do
+ context 'when setting URL for a first time' do
+ it 'stores the URL without credentials' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(mirror.read_attribute(:url)).to eq('http://test.com')
+ end
+
+ it 'stores the credentials on a separate field' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' })
+ end
+
+ it 'handles credentials with large content' do
+ mirror = create_mirror(url: 'http://bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif:9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75@test.com')
+
+ expect(mirror.credentials).to eq({
+ user: 'bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif',
+ password: '9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75'
+ })
+ end
+ end
+
+ context 'when updating the URL' do
+ it 'allows a new URL without credentials' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ mirror.update_attribute(:url, 'http://test.com')
+
+ expect(mirror.url).to eq('http://test.com')
+ expect(mirror.credentials).to eq({ user: nil, password: nil })
+ end
+
+ it 'allows a new URL with credentials' do
+ mirror = create_mirror(url: 'http://test.com')
+
+ mirror.update_attribute(:url, 'http://foo:bar@test.com')
+
+ expect(mirror.url).to eq('http://foo:bar@test.com')
+ expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' })
+ end
+
+ it 'updates the remote config if credentials changed' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+ repo = mirror.project.repository
+
+ mirror.update_attribute(:url, 'http://foo:baz@test.com')
+
+ config = repo.raw_repository.rugged.config
+ expect(config["remote.#{mirror.remote_name}.url"]).to eq('http://foo:baz@test.com')
+ end
+
+ it 'removes previous remote' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
+
+ mirror.update_attributes(url: 'http://test.com')
+ end
+ end
+ end
+
+ describe '#remote_name' do
+ context 'when remote name is persisted in the database' do
+ it 'returns remote name with random value' do
+ allow(SecureRandom).to receive(:hex).and_return('secret')
+
+ remote_mirror = create(:remote_mirror)
+
+ expect(remote_mirror.remote_name).to eq("remote_mirror_secret")
+ end
+ end
+
+ context 'when remote name is not persisted in the database' do
+ it 'returns remote name with remote mirror id' do
+ remote_mirror = create(:remote_mirror)
+ remote_mirror.remote_name = nil
+
+ expect(remote_mirror.remote_name).to eq("remote_mirror_#{remote_mirror.id}")
+ end
+ end
+
+ context 'when remote is not persisted in the database' do
+ it 'returns nil' do
+ remote_mirror = build(:remote_mirror, remote_name: nil)
+
+ expect(remote_mirror.remote_name).to be_nil
+ end
+ end
+ end
+
+ describe '#safe_url' do
+ context 'when URL contains credentials' do
+ it 'masks the credentials' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(mirror.safe_url).to eq('http://*****:*****@test.com')
+ end
+ end
+
+ context 'when URL does not contain credentials' do
+ it 'shows the full URL' do
+ mirror = create_mirror(url: 'http://test.com')
+
+ expect(mirror.safe_url).to eq('http://test.com')
+ end
+ end
+ end
+
+ context 'when remote mirror gets destroyed' do
+ it 'removes remote' do
+ mirror = create_mirror(url: 'http://foo:bar@test.com')
+
+ expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
+
+ mirror.destroy!
+ end
+ end
+
+ context 'stuck mirrors' do
+ it 'includes mirrors stuck in started with no last_update_at set' do
+ mirror = create_mirror(url: 'http://cantbeblank',
+ update_status: 'started',
+ last_update_at: nil,
+ updated_at: 25.hours.ago)
+
+ expect(described_class.stuck.last).to eq(mirror)
+ end
+ end
+
+ context '#sync' do
+ let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with remote mirroring disabled' do
+ it 'returns nil' do
+ remote_mirror.update_attributes(enabled: false)
+
+ expect(remote_mirror.sync).to be_nil
+ end
+ end
+
+ context 'with remote mirroring enabled' do
+ context 'with only protected branches enabled' do
+ context 'when it did not update in the last minute' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+
+ context 'when it did update in the last minute' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next minute' do
+ remote_mirror.last_update_started_at = Time.now - 30.seconds
+
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::PROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+ end
+
+ context 'with only protected branches disabled' do
+ before do
+ remote_mirror.only_protected_branches = false
+ end
+
+ context 'when it did not update in the last 5 minutes' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+
+ context 'when it did update within the last 5 minutes' do
+ it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next 5 minutes' do
+ remote_mirror.last_update_started_at = Time.now - 30.seconds
+
+ expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::UNPROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now)
+
+ remote_mirror.sync
+ end
+ end
+ end
+ end
+ end
+
+ context '#updated_since?' do
+ let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
+ let(:timestamp) { Time.now - 5.minutes }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ remote_mirror.update_attributes(last_update_started_at: Time.now)
+ end
+
+ context 'when remote mirror does not have status failed' do
+ it 'returns true when last update started after the timestamp' do
+ expect(remote_mirror.updated_since?(timestamp)).to be true
+ end
+
+ it 'returns false when last update started before the timestamp' do
+ expect(remote_mirror.updated_since?(Time.now + 5.minutes)).to be false
+ end
+ end
+
+ context 'when remote mirror has status failed' do
+ it 'returns false when last update started after the timestamp' do
+ remote_mirror.update_attributes(update_status: 'failed')
+
+ expect(remote_mirror.updated_since?(timestamp)).to be false
+ end
+ end
+ end
+
+ context 'no project' do
+ it 'includes mirror with a project in pending_delete' do
+ mirror = create_mirror(url: 'http://cantbeblank',
+ update_status: 'finished',
+ enabled: true,
+ last_update_at: nil,
+ updated_at: 25.hours.ago)
+ project = mirror.project
+ project.pending_delete = true
+ project.save
+ mirror.reload
+
+ expect(mirror.sync).to be_nil
+ expect(mirror.valid?).to be_truthy
+ expect(mirror.update_status).to eq('finished')
+ end
+ end
+
+ def create_mirror(params)
+ project = FactoryBot.create(:project, :repository)
+ project.remote_mirrors.create!(params)
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index e45fe7db1e7..0ccf55bd895 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -671,7 +671,7 @@ describe Repository do
end
end
- describe "search_files_by_content" do
+ shared_examples "search_files_by_content" do
let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
@@ -718,7 +718,7 @@ describe Repository do
end
end
- describe "search_files_by_name" do
+ shared_examples "search_files_by_name" do
let(:results) { repository.search_files_by_name('files', 'master') }
it 'returns result' do
@@ -758,6 +758,48 @@ describe Repository do
end
end
+ describe 'with gitaly enabled' do
+ it_behaves_like 'search_files_by_content'
+ it_behaves_like 'search_files_by_name'
+ end
+
+ describe 'with gitaly disabled', :disable_gitaly do
+ it_behaves_like 'search_files_by_content'
+ it_behaves_like 'search_files_by_name'
+ end
+
+ describe '#async_remove_remote' do
+ before do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('joe', 'remote_branch', masterrev)
+ end
+
+ context 'when worker is scheduled successfully' do
+ before do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('remote_name', 'remote_branch', masterrev)
+
+ allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return('1234')
+ end
+
+ it 'returns job_id' do
+ expect(repository.async_remove_remote('joe')).to eq('1234')
+ end
+ end
+
+ context 'when worker does not schedule successfully' do
+ before do
+ allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(Rails.logger).to receive(:info).with("Remove remote job failed to create for #{project.id} with remote name joe.")
+
+ expect(repository.async_remove_remote('joe')).to be_nil
+ end
+ end
+ end
+
describe '#fetch_ref' do
let(:broken_repository) { create(:project, :broken_storage).repository }
@@ -958,65 +1000,25 @@ describe Repository do
subject { repository.add_branch(user, branch_name, target) }
- context 'with Gitaly enabled' do
- it "calls Gitaly's OperationService" do
- expect_any_instance_of(Gitlab::GitalyClient::OperationService)
- .to receive(:user_create_branch).with(branch_name, user, target)
- .and_return(nil)
-
- subject
- end
-
- it 'creates_the_branch' do
- expect(subject.name).to eq(branch_name)
- expect(repository.find_branch(branch_name)).not_to be_nil
- end
-
- context 'with a non-existing target' do
- let(:target) { 'fake-target' }
+ it "calls Gitaly's OperationService" do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService)
+ .to receive(:user_create_branch).with(branch_name, user, target)
+ .and_return(nil)
- it "returns false and doesn't create the branch" do
- expect(subject).to be(false)
- expect(repository.find_branch(branch_name)).to be_nil
- end
- end
+ subject
end
- context 'with Gitaly disabled', :disable_gitaly do
- context 'when pre hooks were successful' do
- it 'runs without errors' do
- hook = double(trigger: [true, nil])
- expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
-
- expect { subject }.not_to raise_error
- end
-
- it 'creates the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
-
- expect(subject.name).to eq(branch_name)
- end
-
- it 'calls the after_create_branch hook' do
- expect(repository).to receive(:after_create_branch)
-
- subject
- end
- end
-
- context 'when pre hooks failed' do
- it 'gets an error' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
-
- expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- end
+ it 'creates_the_branch' do
+ expect(subject.name).to eq(branch_name)
+ expect(repository.find_branch(branch_name)).not_to be_nil
+ end
- it 'does not create the branch' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+ context 'with a non-existing target' do
+ let(:target) { 'fake-target' }
- expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
- expect(repository.find_branch(branch_name)).to be_nil
- end
+ it "returns false and doesn't create the branch" do
+ expect(subject).to be(false)
+ expect(repository.find_branch(branch_name)).to be_nil
end
end
end
@@ -1224,15 +1226,15 @@ describe Repository do
end
end
- shared_examples 'repo exists check' do
+ describe '#exists?' do
it 'returns true when a repository exists' do
- expect(repository.exists?).to eq(true)
+ expect(repository.exists?).to be(true)
end
it 'returns false if no full path can be constructed' do
allow(repository).to receive(:full_path).and_return(nil)
- expect(repository.exists?).to eq(false)
+ expect(repository.exists?).to be(false)
end
context 'with broken storage', :broken_storage do
@@ -1242,16 +1244,6 @@ describe Repository do
end
end
- describe '#exists?' do
- context 'when repository_exists is disabled' do
- it_behaves_like 'repo exists check'
- end
-
- context 'when repository_exists is enabled', :skip_gitaly_mock do
- it_behaves_like 'repo exists check'
- end
- end
-
describe '#has_visible_content?' do
before do
# If raw_repository.has_visible_content? gets called more than once then
@@ -1711,7 +1703,8 @@ describe Repository do
:gitlab_ci,
:avatar,
:issue_template,
- :merge_request_template
+ :merge_request_template,
+ :xcode_config
])
repository.after_change_head
@@ -2036,6 +2029,36 @@ describe Repository do
end
end
+ describe '#xcode_project?' do
+ before do
+ allow(repository).to receive(:tree).with(:head).and_return(double(:tree, trees: [tree]))
+ end
+
+ context 'when the root contains a *.xcodeproj directory' do
+ let(:tree) { double(:tree, path: 'Foo.xcodeproj') }
+
+ it 'returns true' do
+ expect(repository.xcode_project?).to be_truthy
+ end
+ end
+
+ context 'when the root contains a *.xcworkspace directory' do
+ let(:tree) { double(:tree, path: 'Foo.xcworkspace') }
+
+ it 'returns true' do
+ expect(repository.xcode_project?).to be_truthy
+ end
+ end
+
+ context 'when the root contains no Xcode config directory' do
+ let(:tree) { double(:tree, path: 'Foo') }
+
+ it 'returns false' do
+ expect(repository.xcode_project?).to be_falsey
+ end
+ end
+ end
+
describe "#keep_around" do
it "does not fail if we attempt to reference bad commit" do
expect(repository.kept_around?('abc1234')).to be_falsey
@@ -2348,6 +2371,11 @@ describe Repository do
end
end
+ def create_remote_branch(remote_name, branch_name, target)
+ rugged = repository.rugged
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
+ end
+
describe '#ancestor?' do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
diff --git a/spec/models/term_agreement_spec.rb b/spec/models/term_agreement_spec.rb
new file mode 100644
index 00000000000..a59bf119692
--- /dev/null
+++ b/spec/models/term_agreement_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+describe TermAgreement do
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:term) }
+ it { is_expected.to validate_presence_of(:user) }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 35db7616efb..6a2f4a39f09 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe User do
include ProjectForksHelper
+ include TermsHelper
describe 'modules' do
subject { described_class }
@@ -38,7 +39,7 @@ describe User do
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
- it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
@@ -392,24 +393,6 @@ describe User do
end
describe 'after commit hook' do
- describe '.update_invalid_gpg_signatures' do
- let(:user) do
- create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
- user.skip_reconfirmation!
- end
- end
-
- it 'does nothing when the name is updated' do
- expect(user).not_to receive(:update_invalid_gpg_signatures)
- user.update_attributes!(name: 'Bette')
- end
-
- it 'synchronizes the gpg keys when the email is updated' do
- expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
- user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
- end
- end
-
describe '#update_emails_with_primary_email' do
before do
@user = create(:user, email: 'primary@example.com').tap do |user|
@@ -449,6 +432,76 @@ describe User do
expect(@user.emails.first.confirmed_at).not_to eq nil
end
end
+
+ describe '#update_notification_email' do
+ # Regression: https://gitlab.com/gitlab-org/gitlab-ce/issues/22846
+ context 'when changing :email' do
+ let(:user) { create(:user) }
+ let(:new_email) { 'new-email@example.com' }
+
+ it 'sets :unconfirmed_email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.to change(user, :unconfirmed_email).to(new_email)
+ end
+
+ it 'does not change :notification_email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.not_to change(user, :notification_email)
+ end
+
+ it 'updates :notification_email to the new email once confirmed' do
+ user.update!(email: new_email)
+
+ expect do
+ user.tap(&:confirm).reload
+ end.to change(user, :notification_email).to eq(new_email)
+ end
+
+ context 'and :notification_email is set to a secondary email' do
+ let!(:email_attrs) { attributes_for(:email, :confirmed, user: user) }
+ let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
+
+ before do
+ user.emails.create(email_attrs)
+ user.tap { |u| u.update!(notification_email: email_attrs[:email]) }.reload
+ end
+
+ it 'does not change :notification_email to :email' do
+ expect do
+ user.tap { |u| u.update!(email: new_email) }.reload
+ end.not_to change(user, :notification_email)
+ end
+
+ it 'does not change :notification_email to :email once confirmed' do
+ user.update!(email: new_email)
+
+ expect do
+ user.tap(&:confirm).reload
+ end.not_to change(user, :notification_email)
+ end
+ end
+ end
+ end
+
+ describe '#update_invalid_gpg_signatures' do
+ let(:user) do
+ create(:user, email: 'tula.torphy@abshire.ca').tap do |user|
+ user.skip_reconfirmation!
+ end
+ end
+
+ it 'does nothing when the name is updated' do
+ expect(user).not_to receive(:update_invalid_gpg_signatures)
+ user.update_attributes!(name: 'Bette')
+ end
+
+ it 'synchronizes the gpg keys when the email is updated' do
+ expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
+ user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
+ end
+ end
end
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
@@ -1164,8 +1217,12 @@ describe User do
end
context 'with a group route matching the given path' do
+ let!(:group) { create(:group, path: 'group_path') }
+
context 'when the group namespace has an owner_id (legacy data)' do
- let!(:group) { create(:group, path: 'group_path', owner: user) }
+ before do
+ group.update!(owner_id: user.id)
+ end
it 'returns nil' do
expect(described_class.find_by_full_path('group_path')).to eq(nil)
@@ -1173,8 +1230,6 @@ describe User do
end
context 'when the group namespace does not have an owner_id' do
- let!(:group) { create(:group, path: 'group_path') }
-
it 'returns nil' do
expect(described_class.find_by_full_path('group_path')).to eq(nil)
end
@@ -1220,6 +1275,24 @@ describe User do
end
end
+ describe '#accept_pending_invitations!' do
+ let(:user) { create(:user, email: 'user@email.com') }
+ let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) }
+ let!(:group_member_invite) { create(:group_member, :invited, invite_email: user.email) }
+ let!(:external_project_member_invite) { create(:project_member, :invited, invite_email: 'external@email.com') }
+ let!(:external_group_member_invite) { create(:group_member, :invited, invite_email: 'external@email.com') }
+
+ it 'accepts all the user members pending invitations and returns the accepted_members' do
+ accepted_members = user.accept_pending_invitations!
+
+ expect(accepted_members).to match_array([project_member_invite, group_member_invite])
+ expect(group_member_invite.reload).not_to be_invite
+ expect(project_member_invite.reload).not_to be_invite
+ expect(external_project_member_invite.reload).to be_invite
+ expect(external_group_member_invite.reload).to be_invite
+ end
+ end
+
describe '#all_emails' do
let(:user) { create(:user) }
@@ -1783,28 +1856,54 @@ describe User do
end
end
- describe '#ci_authorized_runners' do
+ describe '#ci_owned_runners' do
let(:user) { create(:user) }
- let(:runner) { create(:ci_runner) }
-
- before do
- project.runners << runner
- end
+ let(:runner_1) { create(:ci_runner) }
+ let(:runner_2) { create(:ci_runner) }
- context 'without any projects' do
- let(:project) { create(:project) }
+ context 'without any projects nor groups' do
+ let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:group) { create(:group) }
it 'does not load' do
- expect(user.ci_authorized_runners).to be_empty
+ expect(user.ci_owned_runners).to be_empty
end
end
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
- let(:project) { create(:project, namespace: namespace) }
+ let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
+
+ it 'loads' do
+ expect(user.ci_owned_runners).to contain_exactly(runner_1)
+ end
+ end
+
+ context 'with personal group runner' do
+ let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:group) do
+ create(:group, runners: [runner_2]).tap do |group|
+ group.add_owner(user)
+ end
+ end
+
+ it 'loads' do
+ expect(user.ci_owned_runners).to contain_exactly(runner_2)
+ end
+ end
+
+ context 'with personal project and group runner' do
+ let(:namespace) { create(:namespace, owner: user) }
+ let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
+
+ let!(:group) do
+ create(:group, runners: [runner_2]).tap do |group|
+ group.add_owner(user)
+ end
+ end
it 'loads' do
- expect(user.ci_authorized_runners).to contain_exactly(runner)
+ expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2)
end
end
@@ -1815,7 +1914,7 @@ describe User do
end
it 'loads' do
- expect(user.ci_authorized_runners).to contain_exactly(runner)
+ expect(user.ci_owned_runners).to contain_exactly(runner_1)
end
end
@@ -1825,14 +1924,28 @@ describe User do
end
it 'does not load' do
- expect(user.ci_authorized_runners).to be_empty
+ expect(user.ci_owned_runners).to be_empty
end
end
end
context 'with groups projects runners' do
let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
+ let!(:project) { create(:project, group: group, runners: [runner_1]) }
+
+ def add_user(access)
+ group.add_user(user, access)
+ end
+
+ it_behaves_like :member
+ end
+
+ context 'with groups runners' do
+ let!(:group) do
+ create(:group, runners: [runner_1]).tap do |group|
+ group.add_owner(user)
+ end
+ end
def add_user(access)
group.add_user(user, access)
@@ -1842,7 +1955,7 @@ describe User do
end
context 'with other projects runners' do
- let(:project) { create(:project) }
+ let!(:project) { create(:project, runners: [runner_1]) }
def add_user(access)
project.add_role(user, access)
@@ -1855,7 +1968,7 @@ describe User do
let(:group) { create(:group) }
let(:another_user) { create(:user) }
let(:subgroup) { create(:group, parent: group) }
- let(:project) { create(:project, group: subgroup) }
+ let!(:project) { create(:project, group: subgroup, runners: [runner_1]) }
def add_user(access)
group.add_user(user, access)
@@ -2726,4 +2839,52 @@ describe User do
.to change { RedirectRoute.where(path: 'foo').count }.by(-1)
end
end
+
+ describe '#required_terms_not_accepted?' do
+ let(:user) { build(:user) }
+ subject { user.required_terms_not_accepted? }
+
+ context "when terms are not enforced" do
+ it { is_expected.to be_falsy }
+ end
+
+ context "when terms are enforced and accepted by the user" do
+ before do
+ enforce_terms
+ accept_terms(user)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+
+ context "when terms are enforced but the user has not accepted" do
+ before do
+ enforce_terms
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#increment_failed_attempts!' do
+ subject(:user) { create(:user, failed_attempts: 0) }
+
+ it 'logs failed sign-in attempts' do
+ expect { user.increment_failed_attempts! }.to change(user, :failed_attempts).from(0).to(1)
+ end
+
+ it 'does not log failed sign-in attempts when in a GitLab read-only instance' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect { user.increment_failed_attempts! }.not_to change(user, :failed_attempts)
+ end
+ end
+
+ context 'with uploads' do
+ it_behaves_like 'model with mounted uploader', false do
+ let(:model_object) { create(:user, :with_avatar) }
+ let(:upload_attribute) { :avatar }
+ let(:uploader_class) { AttachmentUploader }
+ end
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index b2b7721674c..1c765ceac2f 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe WikiPage do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
let(:user) { project.owner }
let(:wiki) { ProjectWiki.new(project, user) }
@@ -561,7 +561,7 @@ describe WikiPage do
end
def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
end
def create_page(name, content)
diff --git a/spec/policies/application_setting/term_policy_spec.rb b/spec/policies/application_setting/term_policy_spec.rb
new file mode 100644
index 00000000000..93b5ebf5f72
--- /dev/null
+++ b/spec/policies/application_setting/term_policy_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe ApplicationSetting::TermPolicy do
+ include TermsHelper
+
+ set(:term) { create(:term) }
+ let(:user) { create(:user) }
+
+ subject(:policy) { described_class.new(user, term) }
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
+ it 'has the correct permissions', :aggregate_failures do
+ is_expected.to be_allowed(:accept_terms)
+ is_expected.to be_allowed(:decline_terms)
+ end
+
+ context 'for anonymous users' do
+ let(:user) { nil }
+
+ it 'has the correct permissions', :aggregate_failures do
+ is_expected.to be_disallowed(:accept_terms)
+ is_expected.to be_disallowed(:decline_terms)
+ end
+ end
+
+ context 'when the terms are not current' do
+ before do
+ create(:term)
+ end
+
+ it 'has the correct permissions', :aggregate_failures do
+ is_expected.to be_disallowed(:accept_terms)
+ is_expected.to be_disallowed(:decline_terms)
+ end
+ end
+
+ context 'when the user already accepted the terms' do
+ before do
+ accept_terms(user)
+ end
+
+ it 'has the correct permissions', :aggregate_failures do
+ is_expected.to be_disallowed(:accept_terms)
+ is_expected.to be_allowed(:decline_terms)
+ end
+ end
+end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 41cf2ef7225..9ca156deaa0 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -94,6 +94,19 @@ describe Ci::BuildPolicy do
end
end
end
+
+ context 'when maintainer is allowed to push to pipeline branch' do
+ let(:project) { create(:project, :public) }
+ let(:owner) { user }
+
+ it 'enables update_build if user is maintainer' do
+ allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
+ allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true)
+
+ expect(policy).to be_allowed :update_build
+ expect(policy).to be_allowed :update_commit_status
+ end
+ end
end
describe 'rules for protected ref' do
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index 48a8064c5fc..a5e509cfa0f 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -62,5 +62,17 @@ describe Ci::PipelinePolicy, :models do
end
end
end
+
+ context 'when maintainer is allowed to push to pipeline branch' do
+ let(:project) { create(:project, :public) }
+ let(:owner) { user }
+
+ it 'enables update_pipeline if user is maintainer' do
+ allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
+ allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true)
+
+ expect(policy).to be_allowed :update_pipeline
+ end
+ end
end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 5b8cf2e6ab5..873673b50ef 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe GlobalPolicy do
+ include TermsHelper
+
let(:current_user) { create(:user) }
let(:user) { create(:user) }
@@ -88,4 +90,94 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:update_custom_attribute) }
end
end
+
+ shared_examples 'access allowed when terms accepted' do |ability|
+ it { is_expected.not_to be_allowed(ability) }
+
+ it "allows #{ability} when the user accepted the terms" do
+ accept_terms(current_user)
+
+ is_expected.to be_allowed(ability)
+ end
+ end
+
+ describe 'API access' do
+ context 'regular user' do
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'regular user' do
+ it_behaves_like 'access allowed when terms accepted', :access_api
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'access allowed when terms accepted', :access_api
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+ end
+ end
+
+ describe 'git access' do
+ describe 'regular user' do
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ describe 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ describe 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ end
+
+ context 'regular user' do
+ it_behaves_like 'access allowed when terms accepted', :access_git
+ end
+
+ context 'admin' do
+ let(:current_user) { create(:admin) }
+
+ it_behaves_like 'access allowed when terms accepted', :access_git
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index b4d25e06d9a..9b5c290b9f9 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -7,9 +7,9 @@ describe GroupPolicy do
let(:master) { create(:user) }
let(:owner) { create(:user) }
let(:admin) { create(:admin) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :private) }
- let(:guest_permissions) { [:read_group, :upload_file, :read_namespace] }
+ let(:guest_permissions) { [:read_label, :read_group, :upload_file, :read_namespace] }
let(:reporter_permissions) { [:admin_label] }
@@ -50,6 +50,7 @@ describe GroupPolicy do
end
context 'with no user' do
+ let(:group) { create(:group, :public) }
let(:current_user) { nil }
it do
@@ -63,6 +64,28 @@ describe GroupPolicy do
end
end
+ context 'has projects' do
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project, namespace: group) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it do
+ expect_allowed(:read_group, :read_label)
+ end
+
+ context 'in subgroups', :nested_groups do
+ let(:subgroup) { create(:group, :private, parent: group) }
+ let(:project) { create(:project, namespace: subgroup) }
+
+ it do
+ expect_allowed(:read_group, :read_label)
+ end
+ end
+ end
+
context 'guests' do
let(:current_user) { guest }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 8b9c4ac0b4b..6609f5f7afd 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -404,7 +404,7 @@ describe ProjectPolicy do
)
end
let(:maintainer_abilities) do
- %w(create_build update_build create_pipeline update_pipeline)
+ %w(create_build create_pipeline)
end
subject { described_class.new(user, project) }
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 6593a6ca3b9..a7a77abc3ee 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -10,28 +10,36 @@ describe UserPolicy do
it { is_expected.to be_allowed(:read_user) }
end
- describe "destroying a user" do
+ shared_examples 'changing a user' do |ability|
context "when a regular user tries to destroy another regular user" do
- it { is_expected.not_to be_allowed(:destroy_user) }
+ it { is_expected.not_to be_allowed(ability) }
end
context "when a regular user tries to destroy themselves" do
let(:current_user) { user }
- it { is_expected.to be_allowed(:destroy_user) }
+ it { is_expected.to be_allowed(ability) }
end
context "when an admin user tries to destroy a regular user" do
let(:current_user) { create(:user, :admin) }
- it { is_expected.to be_allowed(:destroy_user) }
+ it { is_expected.to be_allowed(ability) }
end
context "when an admin user tries to destroy a ghost user" do
let(:current_user) { create(:user, :admin) }
let(:user) { create(:user, :ghost) }
- it { is_expected.not_to be_allowed(:destroy_user) }
+ it { is_expected.not_to be_allowed(ability) }
end
end
+
+ describe "destroying a user" do
+ it_behaves_like 'changing a user', :destroy_user
+ end
+
+ describe "updating a user" do
+ it_behaves_like 'changing a user', :update_user
+ end
end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index cc16d0f156b..efd175247b5 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -10,7 +10,7 @@ describe Ci::BuildPresenter do
end
it 'inherits from Gitlab::View::Presenter::Delegated' do
- expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ expect(described_class.ancestors).to include(Gitlab::View::Presenter::Delegated)
end
describe '#initialize' do
@@ -217,4 +217,39 @@ describe Ci::BuildPresenter do
end
end
end
+
+ describe '#callout_failure_message' do
+ let(:build) { create(:ci_build, :failed, :script_failure) }
+
+ it 'returns a verbose failure reason' do
+ description = subject.callout_failure_message
+ expect(description).to eq('There has been a script failure. Check the job log for more information')
+ end
+ end
+
+ describe '#recoverable?' do
+ let(:build) { create(:ci_build, :failed, :script_failure) }
+
+ context 'when is a script or missing dependency failure' do
+ let(:failure_reasons) { %w(script_failure missing_dependency_failure) }
+
+ it 'should return false' do
+ failure_reasons.each do |failure_reason|
+ build.update_attribute(:failure_reason, failure_reason)
+ expect(presenter.recoverable?).to be_falsy
+ end
+ end
+ end
+
+ context 'when is any other failure type' do
+ let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
+
+ it 'should return true' do
+ failure_reasons.each do |failure_reason|
+ build.update_attribute(:failure_reason, failure_reason)
+ expect(presenter.recoverable?).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb
new file mode 100644
index 00000000000..f81ee44e371
--- /dev/null
+++ b/spec/presenters/commit_status_presenter_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe CommitStatusPresenter do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ subject(:presenter) do
+ described_class.new(build)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index e3b37739e8e..d5fb4a7742c 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -70,6 +70,41 @@ describe MergeRequestPresenter do
end
end
+ describe "#unmergeable_reasons" do
+ let(:presenter) { described_class.new(resource, current_user: user) }
+
+ before do
+ # Mergeable base state
+ allow(resource).to receive(:has_no_commits?).and_return(false)
+ allow(resource).to receive(:source_branch_exists?).and_return(true)
+ allow(resource).to receive(:target_branch_exists?).and_return(true)
+ allow(resource.project.repository).to receive(:can_be_merged?).and_return(true)
+ end
+
+ it "handles mergeable request" do
+ expect(presenter.unmergeable_reasons).to eq([])
+ end
+
+ it "handles no commit" do
+ allow(resource).to receive(:has_no_commits?).and_return(true)
+
+ expect(presenter.unmergeable_reasons).to eq(["no commits"])
+ end
+
+ it "handles branches missing" do
+ allow(resource).to receive(:source_branch_exists?).and_return(false)
+ allow(resource).to receive(:target_branch_exists?).and_return(false)
+
+ expect(presenter.unmergeable_reasons).to eq(["source branch is missing", "target branch is missing"])
+ end
+
+ it "handles merge conflict" do
+ allow(resource.project.repository).to receive(:can_be_merged?).and_return(false)
+
+ expect(presenter.unmergeable_reasons).to eq(["has merge conflicts"])
+ end
+ end
+
describe '#conflict_resolution_path' do
let(:project) { create :project }
let(:user) { create :user }
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 55962f345d4..830d2ee3b20 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -208,6 +208,17 @@ describe ProjectPresenter do
it 'returns nil if user cannot push' do
expect(presenter.new_file_anchor_data).to be_nil
end
+
+ context 'when the project is empty' do
+ let(:project) { create(:project, :empty_repo) }
+
+ # Since we protect the default branch for empty repos
+ it 'is empty for a developer' do
+ project.add_developer(user)
+
+ expect(presenter.new_file_anchor_data).to be_nil
+ end
+ end
end
describe '#readme_anchor_data' do
@@ -321,7 +332,7 @@ describe ProjectPresenter do
expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false,
label: 'Enable Auto DevOps',
- link: presenter.project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')))
+ link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings')))
end
end
end
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 4a44b219a67..ef34192f888 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -2,32 +2,53 @@ require 'spec_helper'
describe API::Discussions do
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, namespace: user.namespace) }
+ let!(:project) { create(:project, :public, :repository, namespace: user.namespace) }
let(:private_user) { create(:user) }
before do
- project.add_reporter(user)
+ project.add_developer(user)
end
- context "when noteable is an Issue" do
+ context 'when noteable is an Issue' do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) }
- it_behaves_like "discussions API", 'projects', 'issues', 'iid' do
+ it_behaves_like 'discussions API', 'projects', 'issues', 'iid' do
let(:parent) { project }
let(:noteable) { issue }
let(:note) { issue_note }
end
end
- context "when noteable is a Snippet" do
+ context 'when noteable is a Snippet' do
let!(:snippet) { create(:project_snippet, project: project, author: user) }
let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: user) }
- it_behaves_like "discussions API", 'projects', 'snippets', 'id' do
+ it_behaves_like 'discussions API', 'projects', 'snippets', 'id' do
let(:parent) { project }
let(:noteable) { snippet }
let(:note) { snippet_note }
end
end
+
+ context 'when noteable is a Merge Request' do
+ let!(:noteable) { create(:merge_request_with_diffs, source_project: project, target_project: project, author: user) }
+ let!(:note) { create(:discussion_note_on_merge_request, noteable: noteable, project: project, author: user) }
+ let!(:diff_note) { create(:diff_note_on_merge_request, noteable: noteable, project: project, author: user) }
+ let(:parent) { project }
+
+ it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid'
+ it_behaves_like 'diff discussions API', 'projects', 'merge_requests', 'iid'
+ it_behaves_like 'resolvable discussions API', 'projects', 'merge_requests', 'iid'
+ end
+
+ context 'when noteable is a Commit' do
+ let!(:noteable) { create(:commit, project: project, author: user) }
+ let!(:note) { create(:discussion_note_on_commit, commit_id: noteable.id, project: project, author: user) }
+ let!(:diff_note) { create(:diff_note_on_commit, commit_id: noteable.id, project: project, author: user) }
+ let(:parent) { project }
+
+ it_behaves_like 'discussions API', 'projects', 'repository/commits', 'id'
+ it_behaves_like 'diff discussions API', 'projects', 'repository/commits', 'id'
+ end
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 53d48a91007..fdddca5d0ef 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -15,7 +15,7 @@ describe API::Environments do
it 'returns project environments' do
project_data_keys = %w(
id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url
+ ssh_url_to_repo http_url_to_repo web_url readme_url
name name_with_namespace
path path_with_namespace
star_count forks_count
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index bb0034e3237..7d923932309 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -738,13 +738,16 @@ describe API::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
- delete api("/groups/#{group1.id}", user1)
+ Sidekiq::Testing.fake! do
+ expect { delete api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(202)
end
it_behaves_like '412 response' do
let(:request) { api("/groups/#{group1.id}", user1) }
+ let(:success_status) { 202 }
end
it "does not remove a group if not an owner" do
@@ -773,7 +776,7 @@ describe API::Groups do
it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 837389451e8..d8a51f36dba 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -6,6 +6,7 @@ describe API::Helpers do
include API::APIGuard::HelperMethods
include described_class
include SentryHelper
+ include TermsHelper
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -163,6 +164,23 @@ describe API::Helpers do
expect { current_user }.to raise_error /403/
end
+ context 'when terms are enforced' do
+ before do
+ enforce_terms
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ end
+
+ it 'returns a 403 when a user has not accepted the terms' do
+ expect { current_user }.to raise_error /must accept the Terms of Service/
+ end
+
+ it 'sets the current user when the user accepted the terms' do
+ accept_terms(user)
+
+ expect(current_user).to eq(user)
+ end
+ end
+
it "sets current_user" do
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index db8c5f963d6..5dc3ddd4b36 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe API::Internal do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:key) { create(:key, user: user) }
- let(:project) { create(:project, :repository, :wiki_repo) }
+ set(:project) { create(:project, :repository, :wiki_repo) }
let(:secret_token) { Gitlab::Shell.secret_token }
let(:gl_repository) { "project-#{project.id}" }
let(:reference_counter) { double('ReferenceCounter') }
@@ -277,7 +277,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).not_to have_an_activity_record
end
@@ -289,7 +289,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).to have_an_activity_record
end
@@ -301,7 +301,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
@@ -320,7 +320,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 90f9c4ad214..3106083293f 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -64,12 +64,32 @@ describe API::Issues do
describe "GET /issues" do
context "when unauthenticated" do
- it "returns authentication error" do
+ it "returns an array of all issues" do
+ get api("/issues"), scope: 'all'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it "returns authentication error without any scope" do
get api("/issues")
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is assigned-to-me" do
+ get api("/issues"), scope: 'assigned-to-me'
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is created-by-me" do
+ get api("/issues"), scope: 'created-by-me'
+
+ expect(response).to have_http_status(401)
end
end
+
context "when authenticated" do
let(:first_issue) { json_response.first }
@@ -106,6 +126,15 @@ describe API::Issues do
it 'returns issues assigned to me' do
issue2 = create(:issue, assignees: [user2], project: project)
+ get api('/issues', user2), scope: 'assigned_to_me'
+
+ expect_paginated_array_response(size: 1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+
+ it 'returns issues assigned to me (kebab-case)' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
get api('/issues', user2), scope: 'assigned-to-me'
expect_paginated_array_response(size: 1)
@@ -379,9 +408,6 @@ describe API::Issues do
end
let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
- before do
- group_project.add_reporter(user)
- end
let(:base_url) { "/groups/#{group.id}/issues" }
context 'when group has subgroups', :nested_groups do
@@ -408,178 +434,201 @@ describe API::Issues do
end
end
- it 'returns all group issues (including opened and closed)' do
- get api(base_url, admin)
+ context 'when user is unauthenticated' do
+ it 'lists all issues in public projects' do
+ get api(base_url)
- expect_paginated_array_response(size: 3)
+ expect_paginated_array_response(size: 2)
+ end
end
- it 'returns group issues without confidential issues for non project members' do
- get api("#{base_url}?state=opened", non_member)
+ context 'when user is a group member' do
+ before do
+ group_project.add_reporter(user)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['title']).to eq(group_issue.title)
- end
+ it 'returns all group issues (including opened and closed)' do
+ get api(base_url, admin)
- it 'returns group confidential issues for author' do
- get api("#{base_url}?state=opened", author)
+ expect_paginated_array_response(size: 3)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group issues without confidential issues for non project members' do
+ get api("#{base_url}?state=opened", non_member)
- it 'returns group confidential issues for assignee' do
- get api("#{base_url}?state=opened", assignee)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['title']).to eq(group_issue.title)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group confidential issues for author' do
+ get api("#{base_url}?state=opened", author)
- it 'returns group issues with confidential issues for project members' do
- get api("#{base_url}?state=opened", user)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group confidential issues for assignee' do
+ get api("#{base_url}?state=opened", assignee)
- it 'returns group confidential issues for admin' do
- get api("#{base_url}?state=opened", admin)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 2)
- end
+ it 'returns group issues with confidential issues for project members' do
+ get api("#{base_url}?state=opened", user)
- it 'returns an array of labeled group issues' do
- get api("#{base_url}?labels=#{group_label.title}", user)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['labels']).to eq([group_label.title])
- end
+ it 'returns group confidential issues for admin' do
+ get api("#{base_url}?state=opened", admin)
- it 'returns an array of labeled group issues where all labels match' do
- get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
+ expect_paginated_array_response(size: 2)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an array of labeled group issues' do
+ get api("#{base_url}?labels=#{group_label.title}", user)
- it 'returns issues matching given search string for title' do
- get api("#{base_url}?search=#{group_issue.title}", user)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ it 'returns an array of labeled group issues where all labels match' do
+ get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
- it 'returns issues matching given search string for description' do
- get api("#{base_url}?search=#{group_issue.description}", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ it 'returns issues matching given search string for title' do
+ get api("#{base_url}?search=#{group_issue.title}", user)
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: group_project)
- label_c = create(:label, title: 'bar', project: group_project)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- create(:label_link, label: label_b, target: group_issue)
- create(:label_link, label: label_c, target: group_issue)
+ it 'returns issues matching given search string for description' do
+ get api("#{base_url}?search=#{group_issue.description}", user)
- get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
- end
+ it 'returns an array of labeled issues when all labels matches' do
+ label_b = create(:label, title: 'foo', project: group_project)
+ label_c = create(:label, title: 'bar', project: group_project)
- it 'returns an array of issues found by iids' do
- get api(base_url, user), iids: [group_issue.iid]
+ create(:label_link, label: label_b, target: group_issue)
+ create(:label_link, label: label_c, target: group_issue)
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
- it 'returns an empty array if iid does not exist' do
- get api(base_url, user), iids: [99999]
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an array of issues found by iids' do
+ get api(base_url, user), iids: [group_issue.iid]
- it 'returns an empty array if no group issue matches labels' do
- get api("#{base_url}?labels=foo,bar", user)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an empty array if iid does not exist' do
+ get api(base_url, user), iids: [99999]
- it 'returns an empty array if no issue matches milestone' do
- get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an empty array if no group issue matches labels' do
+ get api("#{base_url}?labels=foo,bar", user)
- it 'returns an empty array if milestone does not exist' do
- get api("#{base_url}?milestone=foo", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 0)
- end
+ it 'returns an empty array if no issue matches milestone' do
+ get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
- it 'returns an array of issues in given milestone' do
- get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
+ it 'returns an empty array if milestone does not exist' do
+ get api("#{base_url}?milestone=foo", user)
- it 'returns an array of issues matching state in milestone' do
- get api("#{base_url}?milestone=#{group_milestone.title}"\
- '&state=closed', user)
+ expect_paginated_array_response(size: 0)
+ end
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_closed_issue.id)
- end
+ it 'returns an array of issues in given milestone' do
+ get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
- it 'returns an array of issues with no milestone' do
- get api("#{base_url}?milestone=#{no_milestone_title}", user)
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
- expect(response).to have_gitlab_http_status(200)
+ it 'returns an array of issues matching state in milestone' do
+ get api("#{base_url}?milestone=#{group_milestone.title}"\
+ '&state=closed', user)
- expect_paginated_array_response(size: 1)
- expect(json_response.first['id']).to eq(group_confidential_issue.id)
- end
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_closed_issue.id)
+ end
- it 'sorts by created_at descending by default' do
- get api(base_url, user)
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}?milestone=#{no_milestone_title}", user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ expect(response).to have_gitlab_http_status(200)
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_confidential_issue.id)
+ end
- it 'sorts ascending when requested' do
- get api("#{base_url}?sort=asc", user)
+ it 'sorts by created_at descending by default' do
+ get api(base_url, user)
- response_dates = json_response.map { |issue| issue['created_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort)
- end
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
- it 'sorts by updated_at descending when requested' do
- get api("#{base_url}?order_by=updated_at", user)
+ it 'sorts ascending when requested' do
+ get api("#{base_url}?sort=asc", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['created_at'] }
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort)
+ end
- it 'sorts by updated_at ascending when requested' do
- get api("#{base_url}?order_by=updated_at&sort=asc", user)
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}?order_by=updated_at", user)
- response_dates = json_response.map { |issue| issue['updated_at'] }
+ response_dates = json_response.map { |issue| issue['updated_at'] }
- expect_paginated_array_response(size: 3)
- expect(response_dates).to eq(response_dates.sort)
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api("#{base_url}?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect_paginated_array_response(size: 3)
+ expect(response_dates).to eq(response_dates.sort)
+ end
end
end
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
+ context 'when unauthenticated' do
+ it 'returns public project issues' do
+ get api("/projects/#{project.id}/issues")
+
+ expect_paginated_array_response(size: 2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+ end
+
it 'avoids N+1 queries' do
control_count = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/issues", user)
@@ -789,6 +838,14 @@ describe API::Issues do
end
describe "GET /projects/:id/issues/:issue_iid" do
+ context 'when unauthenticated' do
+ it 'returns public issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}")
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
@@ -1581,6 +1638,14 @@ describe API::Issues do
create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
end
+ context 'when unauthenticated' do
+ it 'return public project issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
+
+ expect_paginated_array_response(size: 1)
+ end
+ end
+
it 'returns merge requests that will close issue on merge' do
get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
@@ -1605,6 +1670,14 @@ describe API::Issues do
describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
+ context 'when unauthenticated' do
+ it "returns unautorized" do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
it 'exposes known attributes' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 3ffdfdc0e9a..0a2963452e4 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -281,7 +281,7 @@ describe API::Jobs do
get_artifact_file(artifact)
expect(response).to have_gitlab_http_status(200)
- expect(response.headers)
+ expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
end
@@ -311,7 +311,7 @@ describe API::Jobs do
it 'returns specific job artifacts' do
expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include(download_headers)
+ expect(response.headers.to_h).to include(download_headers)
expect(response.body).to match_file(job.artifacts_file.file.file)
end
end
@@ -462,7 +462,7 @@ describe API::Jobs do
end
it { expect(response).to have_http_status(:ok) }
- it { expect(response.headers).to include(download_headers) }
+ it { expect(response.headers.to_h).to include(download_headers) }
end
context 'when artifacts are stored remotely' do
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
new file mode 100644
index 00000000000..a55796cf343
--- /dev/null
+++ b/spec/requests/api/markdown_spec.rb
@@ -0,0 +1,112 @@
+require "spec_helper"
+
+describe API::Markdown do
+ RSpec::Matchers.define_negated_matcher :exclude, :include
+
+ describe "POST /markdown" do
+ let(:user) {} # No-op. It gets overwritten in the contexts below.
+
+ before do
+ post api("/markdown", user), params
+ end
+
+ shared_examples "rendered markdown text without GFM" do
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to eq("<p>#{text}</p>")
+ end
+ end
+
+ shared_examples "404 Project Not Found" do
+ it "responses with 404 Not Found" do
+ expect(response).to have_http_status(404)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["message"]).to eq("404 Project Not Found")
+ end
+ end
+
+ context "when arguments are invalid" do
+ context "when text is missing" do
+ let(:params) { {} }
+
+ it "responses with 400 Bad Request" do
+ expect(response).to have_http_status(400)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["error"]).to eq("text is missing")
+ end
+ end
+
+ context "when project is not found" do
+ let(:params) { { text: "Hello world!", gfm: true, project: "Dummy project" } }
+
+ it_behaves_like "404 Project Not Found"
+ end
+ end
+
+ context "when arguments are valid" do
+ set(:project) { create(:project) }
+ set(:issue) { create(:issue, project: project) }
+ let(:text) { ":tada: Hello world! :100: #{issue.to_reference}" }
+
+ context "when not using gfm" do
+ context "without project" do
+ let(:params) { { text: text } }
+
+ it_behaves_like "rendered markdown text without GFM"
+ end
+
+ context "with project" do
+ let(:params) { { text: text, project: project.full_path } }
+
+ context "when not authorized" do
+ it_behaves_like "404 Project Not Found"
+ end
+
+ context "when authorized" do
+ let(:user) { project.owner }
+
+ it_behaves_like "rendered markdown text without GFM"
+ end
+ end
+ end
+
+ context "when using gfm" do
+ context "without project" do
+ let(:params) { { text: text, gfm: true } }
+
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to include("Hello world!")
+ .and include('data-name="tada"')
+ .and include('data-name="100"')
+ .and include("#1")
+ .and exclude("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
+ .and exclude("#1</a>")
+ end
+ end
+
+ context "with project" do
+ let(:params) { { text: text, gfm: true, project: project.full_path } }
+ let(:user) { project.owner }
+
+ it "renders markdown text" do
+ expect(response).to have_http_status(201)
+ expect(response.headers["Content-Type"]).to eq("application/json")
+ expect(json_response).to be_a(Hash)
+ expect(json_response["html"]).to include("Hello world!")
+ .and include('data-name="tada"')
+ .and include('data-name="100"')
+ .and include("<a href=\"#{IssuesHelper.url_for_issue(issue.iid, project)}\"")
+ .and include("#1</a>")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index f64623d7018..1eeeb4f1045 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -34,8 +34,7 @@ describe API::MergeRequests do
it 'returns an array of all merge requests' do
get api('/merge_requests', user), scope: 'all'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
end
it "returns authentication error without any scope" do
@@ -50,11 +49,23 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(401)
end
+ it "returns authentication error when scope is assigned_to_me" do
+ get api("/merge_requests"), scope: 'assigned_to_me'
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
it "returns authentication error when scope is created-by-me" do
get api("/merge_requests"), scope: 'created-by-me'
expect(response).to have_gitlab_http_status(401)
end
+
+ it "returns authentication error when scope is created_by_me" do
+ get api("/merge_requests"), scope: 'created_by_me'
+
+ expect(response).to have_gitlab_http_status(401)
+ end
end
context 'when authenticated' do
@@ -62,27 +73,14 @@ describe API::MergeRequests do
let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) }
let(:user2) { create(:user) }
- it 'returns an array of all merge requests' do
- get api('/merge_requests', user), scope: :all
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request.id, merge_request_closed.id, merge_request_merged.id, merge_request2.id)
- end
-
- it 'does not return unauthorized merge requests' do
+ it 'returns an array of all merge requests except unauthorized ones' do
private_project = create(:project, :private)
merge_request3 = create(:merge_request, :simple, source_project: private_project, target_project: private_project, source_branch: 'other-branch')
get api('/merge_requests', user), scope: :all
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.map { |mr| mr['id'] })
- .not_to include(merge_request3.id)
+ expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request)
+ expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id)
end
it 'returns an array of merge requests created by current user if no scope is given' do
@@ -90,10 +88,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns an array of merge requests authored by the given user' do
@@ -101,10 +96,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), author_id: user2.id, scope: :all
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns an array of merge requests assigned to the given user' do
@@ -112,32 +104,39 @@ describe API::MergeRequests do
get api('/merge_requests', user), assignee_id: user2.id, scope: :all
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
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')
+ get api('/merge_requests', user2), scope: 'assigned_to_me'
+
+ expect_response_ordered_exactly(merge_request3)
+ end
+
+ it 'returns an array of merge requests assigned to me (kebab-case)' do
+ merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
get api('/merge_requests', user2), scope: 'assigned-to-me'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns an array of merge requests created by me' do
merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ get api('/merge_requests', user2), scope: 'created_by_me'
+
+ expect_response_ordered_exactly(merge_request3)
+ end
+
+ it 'returns an array of merge requests created by me (kebab-case)' do
+ merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+
get api('/merge_requests', user2), scope: 'created-by-me'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
it 'returns merge requests reacted by the authenticated user by the given emoji' do
@@ -146,19 +145,14 @@ describe API::MergeRequests do
get api('/merge_requests', user2), my_reaction_emoji: award_emoji.name, scope: 'all'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request3.id)
+ expect_response_ordered_exactly(merge_request3)
end
context 'source_branch param' do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
@@ -166,9 +160,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
@@ -177,8 +169,7 @@ describe API::MergeRequests do
get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
it 'returns merge requests created after a specific date' do
@@ -186,8 +177,7 @@ describe API::MergeRequests do
get api("/merge_requests?created_after=#{merge_request2.created_at}", user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
it 'returns merge requests updated before a specific date' do
@@ -195,8 +185,7 @@ describe API::MergeRequests do
get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
it 'returns merge requests updated after a specific date' do
@@ -204,8 +193,7 @@ describe API::MergeRequests do
get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user)
- expect(json_response.size).to eq(1)
- expect(json_response.first['id']).to eq(merge_request2.id)
+ expect_response_ordered_exactly(merge_request2)
end
context 'search params' do
@@ -216,15 +204,13 @@ describe API::MergeRequests do
it 'returns merge requests matching given search string for title' do
get api("/merge_requests", user), search: merge_request.title
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request.id)
+ expect_response_ordered_exactly(merge_request)
end
it 'returns merge requests for project matching given search string for description' do
get api("/merge_requests", user), project_id: project.id, search: merge_request.description
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request.id)
+ expect_response_ordered_exactly(merge_request)
end
end
end
@@ -235,8 +221,7 @@ describe API::MergeRequests do
it 'returns merge requests for public projects' do
get api("/projects/#{project.id}/merge_requests")
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
end
it "returns 404 for non public projects" do
@@ -265,10 +250,7 @@ describe API::MergeRequests do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests", 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.length).to eq(3)
+ expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
@@ -286,11 +268,8 @@ describe API::MergeRequests do
it "returns an array of all merge_requests using simple mode" do
get api("/projects/#{project.id}/merge_requests?view=simple", user)
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
+ expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
expect(json_response.last['iid']).to eq(merge_request.iid)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
@@ -302,51 +281,36 @@ describe API::MergeRequests do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", 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.length).to eq(3)
+ expect_response_ordered_exactly(merge_request_merged, merge_request_closed, merge_request)
expect(json_response.last['title']).to eq(merge_request.title)
end
it "returns an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", 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.length).to eq(1)
+ expect_response_ordered_exactly(merge_request)
expect(json_response.last['title']).to eq(merge_request.title)
end
it "returns an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", 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.length).to eq(1)
+ expect_response_ordered_exactly(merge_request_closed)
expect(json_response.first['title']).to eq(merge_request_closed.title)
end
it "returns an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", 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.length).to eq(1)
+ expect_response_ordered_exactly(merge_request_merged)
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
it 'returns merge_request by "iids" array' do
get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_response_ordered_exactly(merge_request_closed, merge_request)
expect(json_response.first['title']).to eq merge_request_closed.title
- expect(json_response.first['id']).to eq merge_request_closed.id
end
it 'matches V4 response schema' do
@@ -359,16 +323,14 @@ describe API::MergeRequests do
it 'returns an empty array if no issue matches milestone' do
get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
it 'returns an empty array if milestone does not exist' do
get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
@@ -382,17 +344,13 @@ describe API::MergeRequests do
it 'returns an array of merge requests matching state in milestone' do
get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request_closed.id)
+ expect_response_ordered_exactly(merge_request_closed)
end
it 'returns an array of labeled merge requests' do
get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label2.title, label.title])
end
@@ -400,16 +358,14 @@ describe API::MergeRequests do
it 'returns an array of labeled merge requests where all labels match' do
get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
it 'returns an empty array if no merge request matches labels' do
get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect_paginated_array_response
expect(json_response.length).to eq(0)
end
@@ -427,13 +383,12 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(mr2.id)
+ expect_response_ordered_exactly(mr2)
end
context "with ordering" do
+ let(:merge_requests) { [merge_request_merged, merge_request_closed, merge_request] }
+
before do
@mr_later = mr_with_later_created_and_updated_at_time
@mr_earlier = mr_with_earlier_created_and_updated_at_time
@@ -442,45 +397,25 @@ describe API::MergeRequests do
it "returns an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", 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.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] })
end
it "returns an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", 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.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] }.reverse)
end
it "returns an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", 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.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['updated_at'] }.reverse)
end
it "returns an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", 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.length).to eq(3)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
+ expect_response_ordered_exactly(*merge_requests.sort_by { |mr| mr['created_at'] })
end
end
@@ -488,9 +423,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
@@ -498,9 +431,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect(json_response.length).to eq(2)
- expect(json_response.map { |mr| mr['id'] })
- .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged)
end
end
end
@@ -1341,4 +1272,22 @@ describe API::MergeRequests do
merge_request_closed.save
merge_request_closed
end
+
+ def expect_response_contain_exactly(*items)
+ expect_paginated_array_response
+ expect(json_response.length).to eq(items.size)
+ expect(json_response.map { |element| element['id'] }).to contain_exactly(*items.map(&:id))
+ end
+
+ def expect_response_ordered_exactly(*items)
+ expect_paginated_array_response
+ expect(json_response.length).to eq(items.size)
+ expect(json_response.map { |element| element['id'] }).to eq(items.map(&:id))
+ end
+
+ def expect_paginated_array_response
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index f68057a92a1..f8c64f063af 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -145,7 +145,7 @@ describe API::ProjectImport do
describe 'GET /projects/:id/import' do
it 'returns the import status' do
- project = create(:project, import_status: 'started')
+ project = create(:project, :import_started)
project.add_master(user)
get api("/projects/#{project.id}/import", user)
@@ -155,8 +155,9 @@ describe API::ProjectImport do
end
it 'returns the import status and the error if failed' do
- project = create(:project, import_status: 'failed', import_error: 'error')
+ project = create(:project, :import_failed)
project.add_master(user)
+ project.import_state.update_attributes(last_error: 'error')
get api("/projects/#{project.id}/import", user)
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
new file mode 100644
index 00000000000..07a920f8d28
--- /dev/null
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe API::ProjectSnapshots do
+ include WorkhorseHelpers
+
+ let(:project) { create(:project) }
+ let(:admin) { create(:admin) }
+
+ describe 'GET /projects/:id/snapshot' do
+ def expect_snapshot_response_for(repository)
+ type, params = workhorse_send_data
+
+ expect(type).to eq('git-snapshot')
+ expect(params).to eq(
+ 'GitalyServer' => {
+ 'address' => Gitlab::GitalyClient.address(repository.project.repository_storage),
+ 'token' => Gitlab::GitalyClient.token(repository.project.repository_storage)
+ },
+ 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new(
+ repository: repository.gitaly_repository
+ ).to_json
+ )
+ end
+
+ it 'returns authentication error as project owner' do
+ get api("/projects/#{project.id}/snapshot", project.owner)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'returns authentication error as unauthenticated user' do
+ get api("/projects/#{project.id}/snapshot", nil)
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
+ it 'requests project repository raw archive as administrator' do
+ get api("/projects/#{project.id}/snapshot", admin), wiki: '0'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect_snapshot_response_for(project.repository)
+ end
+
+ it 'requests wiki repository raw archive as administrator' do
+ get api("/projects/#{project.id}/snapshot", admin), wiki: '1'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect_snapshot_response_for(project.wiki.repository)
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 17272cb00e5..9b7c3205c1f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -18,7 +18,7 @@ describe API::Projects do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:project2) { create(:project, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
@@ -220,7 +220,7 @@ describe API::Projects do
it 'returns a simplified version of all the projects' do
expected_keys = %w(
id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url
+ ssh_url_to_repo http_url_to_repo web_url readme_url
name name_with_namespace
path path_with_namespace
star_count forks_count
@@ -685,7 +685,8 @@ describe API::Projects do
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
- request_access_enabled: true
+ request_access_enabled: true,
+ jobs_enabled: true
})
post api("/projects/user/#{user.id}", admin), project
@@ -853,6 +854,7 @@ describe API::Projects do
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
expect(json_response['merge_method']).to eq(project.merge_method.to_s)
+ expect(json_response['readme_url']).to eq(project.readme_url)
end
it 'returns a project by path name' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 17c7a511857..efb9bddde44 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
-describe API::Runner do
+describe API::Runner, :clean_gitlab_redis_shared_state do
include StubGitlabCalls
+ include RedisHelpers
let(:registration_token) { 'abcdefg123456' }
before do
+ stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
allow_any_instance_of(Ci::Runner).to receive(:cache_attributes)
@@ -39,19 +41,38 @@ describe API::Runner do
expect(json_response['id']).to eq(runner.id)
expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true
+ expect(runner.active).to be true
expect(runner.token).not_to eq(registration_token)
+ expect(runner).to be_instance_type
end
context 'when project token is used' do
let(:project) { create(:project) }
- it 'creates runner' do
+ it 'creates project runner' do
post api('/runners'), token: project.runners_token
expect(response).to have_gitlab_http_status 201
expect(project.runners.size).to eq(1)
- expect(Ci::Runner.first.token).not_to eq(registration_token)
- expect(Ci::Runner.first.token).not_to eq(project.runners_token)
+ runner = Ci::Runner.first
+ expect(runner.token).not_to eq(registration_token)
+ expect(runner.token).not_to eq(project.runners_token)
+ expect(runner).to be_project_type
+ end
+ end
+
+ context 'when group token is used' do
+ let(:group) { create(:group) }
+
+ it 'creates a group runner' do
+ post api('/runners'), token: group.runners_token
+
+ expect(response).to have_http_status 201
+ expect(group.runners.size).to eq(1)
+ runner = Ci::Runner.first
+ expect(runner.token).not_to eq(registration_token)
+ expect(runner.token).not_to eq(group.runners_token)
+ expect(runner).to be_group_type
end
end
end
@@ -109,6 +130,28 @@ describe API::Runner do
end
end
+ context 'when option for activating a Runner is provided' do
+ context 'when active is set to true' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ active: true
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.active).to be true
+ end
+ end
+
+ context 'when active is set to false' do
+ it 'creates runner' do
+ post api('/runners'), token: registration_token,
+ active: false
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.active).to be false
+ end
+ end
+ end
+
context 'when maximum job timeout is specified' do
it 'creates runner' do
post api('/runners'), token: registration_token,
@@ -864,6 +907,65 @@ describe API::Runner do
expect(response.status).to eq(403)
end
end
+
+ context 'when trace is patched' do
+ before do
+ patch_the_trace
+ end
+
+ it 'has valid trace' do
+ expect(response.status).to eq(202)
+ expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when job is cancelled' do
+ before do
+ job.cancel
+ end
+
+ context 'when trace is patched' do
+ before do
+ patch_the_trace
+ end
+
+ it 'returns Forbidden ' do
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'when redis data are flushed' do
+ before do
+ redis_shared_state_cleanup!
+ end
+
+ it 'has empty trace' do
+ expect(job.reload.trace.raw).to eq ''
+ end
+
+ context 'when we perform partial patch' do
+ before do
+ patch_the_trace('hello', headers.merge({ 'Content-Range' => "28-32/5" }))
+ end
+
+ it 'returns an error' do
+ expect(response.status).to eq(416)
+ expect(response.header['Range']).to eq('0-0')
+ end
+ end
+
+ context 'when we resend full trace' do
+ before do
+ patch_the_trace('BUILD TRACE appended appended hello', headers.merge({ 'Content-Range' => "0-34/35" }))
+ end
+
+ it 'succeeds with updating trace' do
+ expect(response.status).to eq(202)
+ expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended hello'
+ end
+ end
+ end
+ end
end
context 'when Runner makes a force-patch' do
@@ -880,7 +982,7 @@ describe API::Runner do
end
context 'when content-range start is too big' do
- let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20/6' }) }
it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
@@ -890,7 +992,7 @@ describe API::Runner do
end
context 'when content-range start is too small' do
- let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20/13' }) }
it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
@@ -1330,7 +1432,7 @@ describe API::Runner do
it 'download artifacts' do
expect(response).to have_http_status(200)
- expect(response.headers).to include download_headers
+ expect(response.headers.to_h).to include download_headers
end
end
@@ -1345,7 +1447,7 @@ describe API::Runner do
it 'uses workhorse send-url' do
expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include(
+ expect(response.headers.to_h).to include(
'Gitlab-Workhorse-Send-Data' => /send-url:/)
end
end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index d30f0cf36e2..c7587c877fc 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -8,22 +8,27 @@ describe API::Runners do
let(:project) { create(:project, creator_id: user.id) }
let(:project2) { create(:project, creator_id: user.id) }
- let!(:shared_runner) { create(:ci_runner, :shared) }
- let!(:unused_specific_runner) { create(:ci_runner) }
+ let(:group) { create(:group).tap { |group| group.add_owner(user) } }
+ let(:group2) { create(:group).tap { |group| group.add_owner(user) } }
- let!(:specific_runner) do
- create(:ci_runner).tap do |runner|
+ let!(:shared_runner) { create(:ci_runner, :shared, description: 'Shared runner') }
+ let!(:unused_project_runner) { create(:ci_runner) }
+
+ let!(:project_runner) do
+ create(:ci_runner, description: 'Project runner').tap do |runner|
create(:ci_runner_project, runner: runner, project: project)
end
end
let!(:two_projects_runner) do
- create(:ci_runner).tap do |runner|
+ create(:ci_runner, description: 'Two projects runner').tap do |runner|
create(:ci_runner_project, runner: runner, project: project)
create(:ci_runner_project, runner: runner, project: project2)
end
end
+ let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group], runner_type: :group_type) }
+
before do
# Set project access for users
create(:project_member, :master, user: user, project: project)
@@ -37,9 +42,14 @@ describe API::Runners do
get api('/runners', user)
shared = json_response.any? { |r| r['is_shared'] }
+ descriptions = json_response.map { |runner| runner['description'] }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
+ expect(descriptions).to contain_exactly(
+ 'Project runner', 'Two projects runner', 'Group runner'
+ )
expect(shared).to be_falsey
end
@@ -50,6 +60,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_falsey
end
@@ -78,6 +89,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_truthy
end
end
@@ -97,6 +109,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_falsey
end
@@ -129,10 +142,16 @@ describe API::Runners do
context 'when runner is not shared' do
it "returns runner's details" do
- get api("/runners/#{specific_runner.id}", admin)
+ get api("/runners/#{project_runner.id}", admin)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['description']).to eq(specific_runner.description)
+ expect(json_response['description']).to eq(project_runner.description)
+ end
+
+ it "returns the project's details for a project runner" do
+ get api("/runners/#{project_runner.id}", admin)
+
+ expect(json_response['projects'].first['id']).to eq(project.id)
end
end
@@ -146,10 +165,10 @@ describe API::Runners do
context "runner project's administrative user" do
context 'when runner is not shared' do
it "returns runner's details" do
- get api("/runners/#{specific_runner.id}", user)
+ get api("/runners/#{project_runner.id}", user)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['description']).to eq(specific_runner.description)
+ expect(json_response['description']).to eq(project_runner.description)
end
end
@@ -164,18 +183,18 @@ describe API::Runners do
end
context 'other authorized user' do
- it "does not return runner's details" do
- get api("/runners/#{specific_runner.id}", user2)
+ it "does not return project runner's details" do
+ get api("/runners/#{project_runner.id}", user2)
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_http_status(403)
end
end
context 'unauthorized user' do
- it "does not return runner's details" do
- get api("/runners/#{specific_runner.id}")
+ it "does not return project runner's details" do
+ get api("/runners/#{project_runner.id}")
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -212,16 +231,16 @@ describe API::Runners do
context 'when runner is not shared' do
it 'updates runner' do
- description = specific_runner.description
- runner_queue_value = specific_runner.ensure_runner_queue_value
+ description = project_runner.description
+ runner_queue_value = project_runner.ensure_runner_queue_value
- update_runner(specific_runner.id, admin, description: 'test')
- specific_runner.reload
+ update_runner(project_runner.id, admin, description: 'test')
+ project_runner.reload
expect(response).to have_gitlab_http_status(200)
- expect(specific_runner.description).to eq('test')
- expect(specific_runner.description).not_to eq(description)
- expect(specific_runner.ensure_runner_queue_value)
+ expect(project_runner.description).to eq('test')
+ expect(project_runner.description).not_to eq(description)
+ expect(project_runner.ensure_runner_queue_value)
.not_to eq(runner_queue_value)
end
end
@@ -247,29 +266,29 @@ describe API::Runners do
end
context 'when runner is not shared' do
- it 'does not update runner without access to it' do
- put api("/runners/#{specific_runner.id}", user2), description: 'test'
+ it 'does not update project runner without access to it' do
+ put api("/runners/#{project_runner.id}", user2), description: 'test'
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_http_status(403)
end
- it 'updates runner with access to it' do
- description = specific_runner.description
- put api("/runners/#{specific_runner.id}", admin), description: 'test'
- specific_runner.reload
+ it 'updates project runner with access to it' do
+ description = project_runner.description
+ put api("/runners/#{project_runner.id}", admin), description: 'test'
+ project_runner.reload
expect(response).to have_gitlab_http_status(200)
- expect(specific_runner.description).to eq('test')
- expect(specific_runner.description).not_to eq(description)
+ expect(project_runner.description).to eq('test')
+ expect(project_runner.description).not_to eq(description)
end
end
end
context 'unauthorized user' do
- it 'does not delete runner' do
- put api("/runners/#{specific_runner.id}")
+ it 'does not delete project runner' do
+ put api("/runners/#{project_runner.id}")
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -293,17 +312,17 @@ describe API::Runners do
context 'when runner is not shared' do
it 'deletes unused runner' do
expect do
- delete api("/runners/#{unused_specific_runner.id}", admin)
+ delete api("/runners/#{unused_project_runner.id}", admin)
expect(response).to have_gitlab_http_status(204)
end.to change { Ci::Runner.specific.count }.by(-1)
end
- it 'deletes used runner' do
+ it 'deletes used project runner' do
expect do
- delete api("/runners/#{specific_runner.id}", admin)
+ delete api("/runners/#{project_runner.id}", admin)
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_http_status(204)
end.to change { Ci::Runner.specific.count }.by(-1)
end
end
@@ -325,34 +344,34 @@ describe API::Runners do
context 'when runner is not shared' do
it 'does not delete runner without access to it' do
- delete api("/runners/#{specific_runner.id}", user2)
+ delete api("/runners/#{project_runner.id}", user2)
expect(response).to have_gitlab_http_status(403)
end
- it 'does not delete runner with more than one associated project' do
+ it 'does not delete project runner with more than one associated project' do
delete api("/runners/#{two_projects_runner.id}", user)
expect(response).to have_gitlab_http_status(403)
end
- it 'deletes runner for one owned project' do
+ it 'deletes project runner for one owned project' do
expect do
- delete api("/runners/#{specific_runner.id}", user)
+ delete api("/runners/#{project_runner.id}", user)
- expect(response).to have_gitlab_http_status(204)
+ expect(response).to have_http_status(204)
end.to change { Ci::Runner.specific.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/runners/#{specific_runner.id}", user) }
+ let(:request) { api("/runners/#{project_runner.id}", user) }
end
end
end
context 'unauthorized user' do
- it 'does not delete runner' do
- delete api("/runners/#{specific_runner.id}")
+ it 'does not delete project runner' do
+ delete api("/runners/#{project_runner.id}")
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(401)
end
end
end
@@ -361,8 +380,8 @@ describe API::Runners do
set(:job_1) { create(:ci_build) }
let!(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) }
let!(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
- let!(:job_4) { create(:ci_build, :running, runner: specific_runner, project: project) }
- let!(:job_5) { create(:ci_build, :failed, runner: specific_runner, project: project) }
+ let!(:job_4) { create(:ci_build, :running, runner: project_runner, project: project) }
+ let!(:job_5) { create(:ci_build, :failed, runner: project_runner, project: project) }
context 'admin user' do
context 'when runner exists' do
@@ -380,7 +399,7 @@ describe API::Runners do
context 'when runner is specific' do
it 'return jobs' do
- get api("/runners/#{specific_runner.id}/jobs", admin)
+ get api("/runners/#{project_runner.id}/jobs", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
@@ -392,7 +411,7 @@ describe API::Runners do
context 'when valid status is provided' do
it 'return filtered jobs' do
- get api("/runners/#{specific_runner.id}/jobs?status=failed", admin)
+ get api("/runners/#{project_runner.id}/jobs?status=failed", admin)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
@@ -405,7 +424,7 @@ describe API::Runners do
context 'when invalid status is provided' do
it 'return 400' do
- get api("/runners/#{specific_runner.id}/jobs?status=non-existing", admin)
+ get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin)
expect(response).to have_gitlab_http_status(400)
end
@@ -433,7 +452,7 @@ describe API::Runners do
context 'when runner is specific' do
it 'return jobs' do
- get api("/runners/#{specific_runner.id}/jobs", user)
+ get api("/runners/#{project_runner.id}/jobs", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
@@ -445,7 +464,7 @@ describe API::Runners do
context 'when valid status is provided' do
it 'return filtered jobs' do
- get api("/runners/#{specific_runner.id}/jobs?status=failed", user)
+ get api("/runners/#{project_runner.id}/jobs?status=failed", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
@@ -458,7 +477,7 @@ describe API::Runners do
context 'when invalid status is provided' do
it 'return 400' do
- get api("/runners/#{specific_runner.id}/jobs?status=non-existing", user)
+ get api("/runners/#{project_runner.id}/jobs?status=non-existing", user)
expect(response).to have_gitlab_http_status(400)
end
@@ -476,7 +495,7 @@ describe API::Runners do
context 'other authorized user' do
it 'does not return jobs' do
- get api("/runners/#{specific_runner.id}/jobs", user2)
+ get api("/runners/#{project_runner.id}/jobs", user2)
expect(response).to have_gitlab_http_status(403)
end
@@ -484,7 +503,7 @@ describe API::Runners do
context 'unauthorized user' do
it 'does not return jobs' do
- get api("/runners/#{specific_runner.id}/jobs")
+ get api("/runners/#{project_runner.id}/jobs")
expect(response).to have_gitlab_http_status(401)
end
@@ -500,6 +519,7 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response[0]).to have_key('ip_address')
expect(shared).to be_truthy
end
end
@@ -523,7 +543,7 @@ describe API::Runners do
describe 'POST /projects/:id/runners' do
context 'authorized user' do
- let(:specific_runner2) do
+ let(:project_runner2) do
create(:ci_runner).tap do |runner|
create(:ci_runner_project, runner: runner, project: project2)
end
@@ -531,23 +551,23 @@ describe API::Runners do
it 'enables specific runner' do
expect do
- post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
+ post api("/projects/#{project.id}/runners", user), runner_id: project_runner2.id
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(201)
end
it 'avoids changes when enabling already enabled runner' do
expect do
- post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
+ post api("/projects/#{project.id}/runners", user), runner_id: project_runner.id
end.to change { project.runners.count }.by(0)
expect(response).to have_gitlab_http_status(409)
end
it 'does not enable locked runner' do
- specific_runner2.update(locked: true)
+ project_runner2.update(locked: true)
expect do
- post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
+ post api("/projects/#{project.id}/runners", user), runner_id: project_runner2.id
end.to change { project.runners.count }.by(0)
expect(response).to have_gitlab_http_status(403)
@@ -559,18 +579,33 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(403)
end
+ it 'does not enable group runner' do
+ post api("/projects/#{project.id}/runners", user), runner_id: group_runner.id
+
+ expect(response).to have_http_status(403)
+ end
+
context 'user is admin' do
it 'enables any specific runner' do
expect do
- post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
+ post api("/projects/#{project.id}/runners", admin), runner_id: unused_project_runner.id
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(201)
end
+
+ it 'enables a shared runner' do
+ expect do
+ post api("/projects/#{project.id}/runners", admin), runner_id: shared_runner.id
+ end.to change { project.runners.count }.by(1)
+
+ expect(shared_runner.reload).not_to be_shared
+ expect(response).to have_gitlab_http_status(201)
+ end
end
context 'user is not admin' do
it 'does not enable runner without access to' do
- post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
+ post api("/projects/#{project.id}/runners", user), runner_id: unused_project_runner.id
expect(response).to have_gitlab_http_status(403)
end
@@ -619,7 +654,7 @@ describe API::Runners do
context 'when runner have one associated projects' do
it "does not disable project's runner" do
expect do
- delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
+ delete api("/projects/#{project.id}/runners/#{project_runner.id}", user)
end.to change { project.runners.count }.by(0)
expect(response).to have_gitlab_http_status(403)
end
@@ -634,7 +669,7 @@ describe API::Runners do
context 'authorized user without permissions' do
it "does not disable project's runner" do
- delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
+ delete api("/projects/#{project.id}/runners/#{project_runner.id}", user2)
expect(response).to have_gitlab_http_status(403)
end
@@ -642,7 +677,7 @@ describe API::Runners do
context 'unauthorized user' do
it "does not disable project's runner" do
- delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
+ delete api("/projects/#{project.id}/runners/#{project_runner.id}")
expect(response).to have_gitlab_http_status(401)
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index f8d5258a8d9..aca4aa40027 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Search do
set(:user) { create(:user) }
set(:group) { create(:group) }
- set(:project) { create(:project, :public, name: 'awesome project', group: group) }
+ set(:project) { create(:project, :wiki_repo, :public, name: 'awesome project', group: group) }
set(:repo_project) { create(:project, :public, :repository, group: group) }
shared_examples 'response is correct' do |schema:, size: 1|
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 015d4b9a491..8b22d1e72f3 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -54,7 +54,9 @@ describe API::Settings, 'Settings' do
dsa_key_restriction: 2048,
ecdsa_key_restriction: 384,
ed25519_key_restriction: 256,
- circuitbreaker_check_interval: 2
+ circuitbreaker_check_interval: 2,
+ enforce_terms: true,
+ terms: 'Hello world!'
expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
@@ -76,6 +78,8 @@ describe API::Settings, 'Settings' do
expect(json_response['ecdsa_key_restriction']).to eq(384)
expect(json_response['ed25519_key_restriction']).to eq(256)
expect(json_response['circuitbreaker_check_interval']).to eq(2)
+ expect(json_response['enforce_terms']).to be(true)
+ expect(json_response['terms']).to eq('Hello world!')
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f406d2ffb22..05637eb0729 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -212,6 +212,18 @@ describe API::Users do
expect(json_response.last['id']).to eq(user.id)
end
+ it 'returns users with 2fa enabled' do
+ admin
+ user
+ user_with_2fa = create(:user, :two_factor_via_otp)
+
+ get api('/users', admin), { two_factor: 'enabled' }
+
+ expect(response).to match_response_schema('public_api/v4/user/admins')
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(user_with_2fa.id)
+ end
+
it 'returns 400 when provided incorrect sort params' do
get api('/users', admin), { order_by: 'magic', sort: 'asc' }
@@ -476,10 +488,6 @@ describe API::Users do
describe "PUT /users/:id" do
let!(:admin_user) { create(:admin) }
- before do
- admin
- end
-
it "updates user with new bio" do
put api("/users/#{user.id}", admin), { bio: 'new test bio' }
@@ -513,27 +521,28 @@ describe API::Users do
expect(json_response['avatar_url']).to include(user.avatar_path)
end
- it 'updates user with his own email' do
- put api("/users/#{user.id}", admin), email: user.email
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['email']).to eq(user.email)
- expect(user.reload.email).to eq(user.email)
- end
-
it 'updates user with a new email' do
+ old_email = user.email
+ old_notification_email = user.notification_email
put api("/users/#{user.id}", admin), email: 'new@email.com'
+ user.reload
+
expect(response).to have_gitlab_http_status(200)
- expect(user.reload.notification_email).to eq('new@email.com')
+ expect(user).to be_confirmed
+ expect(user.email).to eq(old_email)
+ expect(user.notification_email).to eq(old_notification_email)
+ expect(user.unconfirmed_email).to eq('new@email.com')
end
it 'skips reconfirmation when requested' do
- put api("/users/#{user.id}", admin), { skip_reconfirmation: true }
+ put api("/users/#{user.id}", admin), email: 'new@email.com', skip_reconfirmation: true
user.reload
- expect(user.confirmed_at).to be_present
+ expect(response).to have_gitlab_http_status(200)
+ expect(user).to be_confirmed
+ expect(user.email).to eq('new@email.com')
end
it 'updates user with his own username' do
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 00f067889a0..485d7c2cc43 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -232,7 +232,7 @@ describe API::V3::Builds do
it 'returns specific job artifacts' do
expect(response).to have_http_status(200)
- expect(response.headers).to include(download_headers)
+ expect(response.headers.to_h).to include(download_headers)
expect(response.body).to match_file(build.artifacts_file.file.file)
end
end
@@ -332,7 +332,7 @@ describe API::V3::Builds do
end
it { expect(response).to have_http_status(200) }
- it { expect(response.headers).to include(download_headers) }
+ it { expect(response.headers.to_h).to include(download_headers) }
end
context 'when artifacts are stored remotely' do
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index a1cdf583de3..34d4b8e9565 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -458,9 +458,11 @@ describe API::V3::Groups do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
it "removes group" do
- delete v3_api("/groups/#{group1.id}", user1)
+ Sidekiq::Testing.fake! do
+ expect { delete v3_api("/groups/#{group1.id}", user1) }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a group if not an owner" do
@@ -489,7 +491,7 @@ describe API::V3::Groups do
it "removes any existing group" do
delete v3_api("/groups/#{group2.id}", admin)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_gitlab_http_status(202)
end
it "does not remove a non existing group" do
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 4c25bd935c6..158ddf171bc 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -82,7 +82,7 @@ describe API::V3::Projects do
it 'returns a simplified version of all the projects' do
expected_keys = %w(
id description default_branch tag_list
- ssh_url_to_repo http_url_to_repo web_url
+ ssh_url_to_repo http_url_to_repo web_url readme_url
name name_with_namespace
path path_with_namespace
star_count forks_count
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index 7bbf34422b8..38b618191fb 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -18,7 +18,7 @@ describe API::Version do
expect(response).to have_gitlab_http_status(200)
expect(json_response['version']).to eq(Gitlab::VERSION)
- expect(json_response['revision']).to eq(Gitlab::REVISION)
+ expect(json_response['revision']).to eq(Gitlab.revision)
end
end
end
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index fb0806ff9f1..850ba696098 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -143,7 +143,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
context 'when user is guest' do
before do
@@ -175,7 +175,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_repo, :wiki_private) }
context 'when user is guest' do
before do
@@ -203,7 +203,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -236,7 +236,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_repo, :wiki_disabled) }
context 'when user is guest' do
before do
@@ -268,7 +268,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_repo, :wiki_private) }
context 'when user is guest' do
before do
@@ -311,7 +311,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -360,7 +360,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
context 'when user is guest' do
before do
@@ -390,7 +390,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_private, :wiki_repo) }
context 'when user is guest' do
before do
@@ -418,7 +418,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -452,7 +452,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
context 'when user is guest' do
before do
@@ -484,7 +484,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_private, :wiki_repo) }
context 'when user is guest' do
before do
@@ -528,7 +528,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -572,7 +572,7 @@ describe API::Wikis do
end
context 'when wiki belongs to a group project' do
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:project, :wiki_repo, namespace: group) }
before do
put(api(url, user), payload)
@@ -587,7 +587,7 @@ describe API::Wikis do
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
context 'when wiki is disabled' do
- let(:project) { create(:project, :wiki_disabled) }
+ let(:project) { create(:project, :wiki_disabled, :wiki_repo) }
context 'when user is guest' do
before do
@@ -619,7 +619,7 @@ describe API::Wikis do
end
context 'when wiki is available only for team members' do
- let(:project) { create(:project, :wiki_private) }
+ let(:project) { create(:project, :wiki_private, :wiki_repo) }
context 'when user is guest' do
before do
@@ -651,7 +651,7 @@ describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
before do
@@ -689,7 +689,7 @@ describe API::Wikis do
end
context 'when wiki belongs to a group project' do
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:project, :wiki_repo, namespace: group) }
before do
delete(api(url, user))
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 494db30e8e0..2514dab1714 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,6 +1,7 @@
require "spec_helper"
describe 'Git HTTP requests' do
+ include TermsHelper
include GitHttpHelpers
include WorkhorseHelpers
include UserActivitiesHelpers
@@ -824,4 +825,56 @@ describe 'Git HTTP requests' do
end
end
end
+
+ context 'when terms are enforced' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:path) { "#{project.full_path}.git" }
+ let(:env) { { user: user.username, password: user.password } }
+
+ before do
+ project.add_master(user)
+ enforce_terms
+ end
+
+ it 'blocks git access when the user did not accept terms', :aggregate_failures do
+ clone_get(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ download(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ upload(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when the user accepted the terms' do
+ before do
+ accept_terms(user)
+ end
+
+ it 'allows clones' do
+ clone_get(path, env) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it_behaves_like 'pulls are allowed'
+ it_behaves_like 'pushes are allowed'
+ end
+
+ context 'from CI' do
+ let(:build) { create(:ci_build, :running) }
+ let(:env) { { user: 'gitlab-ci-token', password: build.token } }
+
+ before do
+ build.update!(user: user, project: project)
+ end
+
+ it_behaves_like 'pulls are allowed'
+ end
+ end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 6bed8e812c0..be286c490fe 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -153,4 +153,15 @@ describe 'OpenID Connect requests' do
end
end
end
+
+ context 'OpenID configuration information' do
+ it 'correctly returns the configuration' do
+ get '/.well-known/openid-configuration'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['issuer']).to eq('http://localhost')
+ expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
+ expect(json_response['scopes_supported']).to eq(%w[api read_user sudo read_repository openid])
+ end
+ end
end
diff --git a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb b/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb
deleted file mode 100644
index 6d769c8e6fd..00000000000
--- a/spec/rubocop/cop/gitlab/has_many_through_scope_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/gitlab/has_many_through_scope'
-
-describe RuboCop::Cop::Gitlab::HasManyThroughScope do # rubocop:disable RSpec/FilePath
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in a model file' do
- before do
- allow(cop).to receive(:in_model?).and_return(true)
- end
-
- context 'when the model does not use has_many :through' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, source: 'UserTag'
- end
- RUBY
- end
- end
-
- context 'when the model uses has_many :through' do
- context 'when the association has no scope defined' do
- it 'registers an offense on the association' do
- expect_offense(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, through: :user_tags
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
- end
- RUBY
- end
- end
-
- context 'when the association has a scope defined' do
- context 'when the scope does not disable auto-loading' do
- it 'registers an offense on the scope' do
- expect_offense(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, -> { where(active: true) }, through: :user_tags
- ^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
- end
- RUBY
- end
- end
-
- context 'when the scope has auto_include(false)' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, -> { where(active: true).auto_include(false).reorder(nil) }, through: :user_tags
- end
- RUBY
- end
- end
- end
- end
- end
-
- context 'outside of a migration spec file' do
- it 'does not register an offense' do
- expect_no_offenses(<<-RUBY)
- class User < ActiveRecord::Base
- has_many :tags, through: :user_tags
- end
- RUBY
- end
- end
-end
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
index b9cc2f64831..36da8d33a44 100644
--- a/spec/serializers/entity_date_helper_spec.rb
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -32,6 +32,7 @@ describe EntityDateHelper do
end
it 'converts 86560 seconds' do
+ Rails.logger.debug date_helper_class.inspect
expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
end
@@ -42,4 +43,58 @@ describe EntityDateHelper do
it 'converts 986760 seconds' do
expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
end
+
+ describe '#remaining_days_in_words' do
+ around do |example|
+ Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+ end
+
+ context 'when less than 31 days remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
+
+ it 'returns days remaining' do
+ expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
+ end
+ end
+
+ context 'when less than 1 year and more than 30 days remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
+
+ it 'returns months remaining' do
+ expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
+ end
+ end
+
+ context 'when more than 1 year remaining' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
+
+ it 'returns years remaining' do
+ expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
+ end
+ end
+
+ context 'when milestone is expired' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
+
+ it 'returns "Past due"' do
+ expect(milestone_remaining).to eq("<strong>Past due</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the future' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
+
+ it 'returns "Upcoming"' do
+ expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the past' do
+ let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
+
+ it 'returns days elapsed' do
+ expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
+ end
+ end
+ end
end
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 24a6f1a2a8a..c90396ebb28 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -133,22 +133,65 @@ describe JobEntity do
context 'when job failed' do
let(:job) { create(:ci_build, :script_failure) }
- describe 'status' do
- it 'should contain the failure reason inside label' do
- expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
- expect(subject[:status][:label]).to eq('failed')
- expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
- end
+ it 'contains details' do
+ expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
+ end
+
+ it 'states that it failed' do
+ expect(subject[:status][:label]).to eq('failed')
+ end
+
+ it 'should indicate the failure reason on tooltip' do
+ expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)')
+ end
+
+ it 'should include a callout message with a verbose output' do
+ expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ end
+
+ it 'should state that it is not recoverable' do
+ expect(subject[:recoverable]).to be_falsy
+ end
+ end
+
+ context 'when job is allowed to fail' do
+ let(:job) { create(:ci_build, :allowed_to_fail, :script_failure) }
+
+ it 'contains details' do
+ expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip
+ end
+
+ it 'states that it failed' do
+ expect(subject[:status][:label]).to eq('failed (allowed to fail)')
+ end
+
+ it 'should indicate the failure reason on tooltip' do
+ expect(subject[:status][:tooltip]).to eq('failed <br> (script failure) (allowed to fail)')
+ end
+
+ it 'should include a callout message with a verbose output' do
+ expect(subject[:callout_message]).to eq('There has been a script failure. Check the job log for more information')
+ end
+
+ it 'should state that it is not recoverable' do
+ expect(subject[:recoverable]).to be_falsy
+ end
+ end
+
+ context 'when job failed and is recoverable' do
+ let(:job) { create(:ci_build, :api_failure) }
+
+ it 'should state it is recoverable' do
+ expect(subject[:recoverable]).to be_truthy
end
end
context 'when job passed' do
let(:job) { create(:ci_build, :success) }
- describe 'status' do
- it 'should not contain the failure reason inside label' do
- expect(subject[:status][:label]).to eq('passed')
- end
+ it 'should not include callout message or recoverable keys' do
+ expect(subject).not_to include('callout_message')
+ expect(subject).not_to include('recoverable')
end
end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 2473c561f4b..e67d12b7a89 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -26,6 +26,13 @@ describe PipelineEntity do
expect(subject).to include :updated_at, :created_at
end
+ it 'excludes coverage data when disabled' do
+ entity = described_class
+ .represent(pipeline, request: request, disable_coverage: true)
+
+ expect(entity.as_json).not_to include(:coverage)
+ end
+
it 'contains details' do
expect(subject).to include :details
expect(subject[:details])
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index f51c11b141f..b741308e2c5 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -114,21 +114,17 @@ describe PipelineSerializer do
Gitlab::GitalyClient.reset_counts
end
- shared_examples 'no N+1 queries' do
+ context 'with the same ref' do
+ let(:ref) { 'feature' }
+
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(36)
+ expect(recorded.count).to be_within(1).of(44)
expect(recorded.cached_count).to eq(0)
end
end
- context 'with the same ref' do
- let(:ref) { 'feature' }
-
- it_behaves_like 'no N+1 queries'
- end
-
context 'with different refs' do
def ref
@sequence ||= 0
@@ -136,7 +132,16 @@ describe PipelineSerializer do
"feature-#{@sequence}"
end
- it_behaves_like 'no N+1 queries'
+ it 'verifies number of queries', :request_store do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
+
+ # For each ref there is a permission check if maintainer can update
+ # pipeline. With the same ref this check is cached but if refs are
+ # different then there is an extra query per ref
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368
+ expect(recorded.count).to be_within(1).of(51)
+ expect(recorded.cached_count).to eq(0)
+ end
end
def create_pipeline(status)
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
new file mode 100644
index 00000000000..fb07ecc6ae8
--- /dev/null
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe ApplicationSettings::UpdateService do
+ let(:application_settings) { Gitlab::CurrentSettings.current_application_settings }
+ let(:admin) { create(:user, :admin) }
+ let(:params) { {} }
+
+ subject { described_class.new(application_settings, admin, params) }
+
+ before do
+ # So the caching behaves like it would in production
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
+ describe 'updating terms' do
+ context 'when the passed terms are blank' do
+ let(:params) { { terms: '' } }
+
+ it 'does not create terms' do
+ expect { subject.execute }.not_to change { ApplicationSetting::Term.count }
+ end
+ end
+
+ context 'when passing terms' do
+ let(:params) { { terms: 'Be nice! ' } }
+
+ it 'creates the terms' do
+ expect { subject.execute }.to change { ApplicationSetting::Term.count }.by(1)
+ end
+
+ it 'does not create terms if they are the same as the existing ones' do
+ create(:term, terms: 'Be nice!')
+
+ expect { subject.execute }.not_to change { ApplicationSetting::Term.count }
+ end
+
+ it 'updates terms if they already existed' do
+ create(:term, terms: 'Other terms')
+
+ subject.execute
+
+ expect(application_settings.terms).to eq('Be nice!')
+ end
+
+ it 'Only queries once when the terms are changed' do
+ create(:term, terms: 'Other terms')
+ expect(application_settings.terms).to eq('Other terms')
+
+ subject.execute
+
+ expect(application_settings.terms).to eq('Be nice!')
+ expect { 2.times { application_settings.terms } }
+ .not_to exceed_query_limit(0)
+ end
+ end
+ end
+end
diff --git a/spec/services/applications/create_service_spec.rb b/spec/services/applications/create_service_spec.rb
index 47a2a9d6403..9c43b56744b 100644
--- a/spec/services/applications/create_service_spec.rb
+++ b/spec/services/applications/create_service_spec.rb
@@ -1,13 +1,17 @@
-require 'spec_helper'
+require "spec_helper"
describe ::Applications::CreateService do
let(:user) { create(:user) }
let(:params) { attributes_for(:application) }
- let(:request) { ActionController::TestRequest.new(remote_ip: '127.0.0.1') }
+ let(:request) do
+ if Gitlab.rails5?
+ ActionController::TestRequest.new({ remote_ip: "127.0.0.1" }, ActionController::TestSession.new)
+ else
+ ActionController::TestRequest.new(remote_ip: "127.0.0.1")
+ end
+ end
subject { described_class.new(user, params) }
- it 'creates an application' do
- expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1)
- end
+ it { expect { subject.execute(request) }.to change { Doorkeeper::Application.count }.by(1) }
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 267258b33a8..2b88fcc9a96 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -17,11 +17,13 @@ describe Ci::CreatePipelineService do
after: project.commit.id,
message: 'Message',
ref: ref_name,
- trigger_request: nil)
+ trigger_request: nil,
+ variables_attributes: nil)
params = { ref: ref,
before: '00000000',
after: after,
- commits: [{ message: message }] }
+ commits: [{ message: message }],
+ variables_attributes: variables_attributes }
described_class.new(project, user, params).execute(
source, trigger_request: trigger_request)
@@ -393,7 +395,27 @@ describe Ci::CreatePipelineService do
result = execute_service
expect(result).to be_persisted
- expect(Environment.find_by(name: "review/master")).not_to be_nil
+ expect(Environment.find_by(name: "review/master")).to be_present
+ end
+ end
+
+ context 'with environment name including persisted variables' do
+ before do
+ config = YAML.dump(
+ deploy: {
+ environment: { name: "review/id1$CI_PIPELINE_ID/id2$CI_BUILD_ID" },
+ script: 'ls'
+ }
+ )
+
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'skipps persisted variables in environment name' do
+ result = execute_service
+
+ expect(result).to be_persisted
+ expect(Environment.find_by(name: "review/id1/id2")).to be_present
end
end
@@ -545,5 +567,19 @@ describe Ci::CreatePipelineService do
expect(pipeline.tag?).to be true
end
end
+
+ context 'when pipeline variables are specified' do
+ let(:variables_attributes) do
+ [{ key: 'first', secret_value: 'world' },
+ { key: 'second', secret_value: 'second_world' }]
+ end
+
+ subject { execute_service(variables_attributes: variables_attributes) }
+
+ it 'creates a pipeline with specified variables' do
+ expect(subject.variables.map { |var| var.slice(:key, :secret_value) })
+ .to eq variables_attributes.map(&:with_indifferent_access)
+ end
+ end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 8a537e83d5f..8063bc7e1ac 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -2,11 +2,13 @@ require 'spec_helper'
module Ci
describe RegisterJobService do
- let!(:project) { FactoryBot.create :project, shared_runners_enabled: false }
- let!(:pipeline) { FactoryBot.create :ci_pipeline, project: project }
- let!(:pending_job) { FactoryBot.create :ci_build, pipeline: pipeline }
- let!(:shared_runner) { FactoryBot.create(:ci_runner, is_shared: true) }
- let!(:specific_runner) { FactoryBot.create(:ci_runner, is_shared: false) }
+ set(:group) { create(:group) }
+ set(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:shared_runner) { create(:ci_runner, is_shared: true) }
+ let!(:specific_runner) { create(:ci_runner, is_shared: false) }
+ let!(:group_runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
+ let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
before do
specific_runner.assign_to(project)
@@ -150,7 +152,7 @@ module Ci
context 'disallow when builds are disabled' do
before do
- project.update(shared_runners_enabled: true)
+ project.update(shared_runners_enabled: true, group_runners_enabled: true)
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
@@ -160,13 +162,90 @@ module Ci
it { expect(build).to be_nil }
end
- context 'and uses specific runner' do
+ context 'and uses group runner' do
+ let(:build) { execute(group_runner) }
+
+ it { expect(build).to be_nil }
+ end
+
+ context 'and uses project runner' do
let(:build) { execute(specific_runner) }
it { expect(build).to be_nil }
end
end
+ context 'allow group runners' do
+ before do
+ project.update!(group_runners_enabled: true)
+ end
+
+ context 'for multiple builds' do
+ let!(:project2) { create :project, group_runners_enabled: true, group: group }
+ let!(:pipeline2) { create :ci_pipeline, project: project2 }
+ let!(:project3) { create :project, group_runners_enabled: true, group: group }
+ let!(:pipeline3) { create :ci_pipeline, project: project3 }
+
+ let!(:build1_project1) { pending_job }
+ let!(:build2_project1) { create :ci_build, pipeline: pipeline }
+ let!(:build3_project1) { create :ci_build, pipeline: pipeline }
+ let!(:build1_project2) { create :ci_build, pipeline: pipeline2 }
+ let!(:build2_project2) { create :ci_build, pipeline: pipeline2 }
+ let!(:build1_project3) { create :ci_build, pipeline: pipeline3 }
+
+ # these shouldn't influence the scheduling
+ let!(:unrelated_group) { create :group }
+ let!(:unrelated_project) { create :project, group_runners_enabled: true, group: unrelated_group }
+ let!(:unrelated_pipeline) { create :ci_pipeline, project: unrelated_project }
+ let!(:build1_unrelated_project) { create :ci_build, pipeline: unrelated_pipeline }
+ let!(:unrelated_group_runner) { create :ci_runner, groups: [unrelated_group] }
+
+ it 'does not consider builds from other group runners' do
+ expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 6
+ execute(group_runner)
+
+ expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 5
+ execute(group_runner)
+
+ expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 4
+ execute(group_runner)
+
+ expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 3
+ execute(group_runner)
+
+ expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 2
+ execute(group_runner)
+
+ expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 1
+ execute(group_runner)
+
+ expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 0
+ expect(execute(group_runner)).to be_nil
+ end
+ end
+
+ context 'group runner' do
+ let(:build) { execute(group_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(group_runner) }
+ end
+ end
+
+ context 'disallow group runners' do
+ before do
+ project.update!(group_runners_enabled: false)
+ end
+
+ context 'group runner' do
+ let(:build) { execute(group_runner) }
+
+ it { expect(build).to be_nil }
+ end
+ end
+
context 'when first build is stalled' do
before do
pending_job.update(lock_version: 0)
@@ -178,7 +257,7 @@ module Ci
let!(:other_build) { create :ci_build, pipeline: pipeline }
before do
- allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
.and_return(Ci::Build.where(id: [pending_job, other_build]))
end
@@ -190,7 +269,7 @@ module Ci
context 'when single build is in queue' do
before do
- allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
.and_return(Ci::Build.where(id: pending_job))
end
@@ -201,7 +280,7 @@ module Ci
context 'when there is no build in queue' do
before do
- allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
+ allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner)
.and_return(Ci::Build.none)
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 8de0bdf92e2..e1cb7ed8110 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -6,7 +6,9 @@ describe Ci::RetryBuildService do
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:stage) do
- Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test')
+ create(:ci_stage_entity, project: project,
+ pipeline: pipeline,
+ name: 'test')
end
let(:build) { create(:ci_build, pipeline: pipeline, stage_id: stage.id) }
@@ -30,7 +32,7 @@ describe Ci::RetryBuildService do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
artifacts_file_store artifacts_metadata_store
- metadata].freeze
+ metadata trace_chunks].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 6ce75c65c8c..a73bd7a0268 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::RetryPipelineService, '#execute' do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -235,6 +237,8 @@ describe Ci::RetryPipelineService, '#execute' do
context 'when user is not allowed to trigger manual action' do
before do
project.add_developer(user)
+ create(:protected_branch, :masters_can_push,
+ name: pipeline.ref, project: project)
end
context 'when there is a failed manual action present' do
@@ -264,6 +268,33 @@ describe Ci::RetryPipelineService, '#execute' do
end
end
+ context 'when maintainer is allowed to push to forked project' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:forked_project) { fork_project(project) }
+ let(:pipeline) { create(:ci_pipeline, project: forked_project, ref: 'fixes') }
+
+ before do
+ project.add_master(user)
+ create(:merge_request,
+ source_project: forked_project,
+ target_project: project,
+ source_branch: 'fixes',
+ allow_maintainer_to_push: true)
+ create_build('rspec 1', :failed, 1)
+ end
+
+ it 'allows to retry failed pipeline' do
+ allow_any_instance_of(Project).to receive(:fetch_branch_allows_maintainer_push?).and_return(true)
+ allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
+
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(pipeline.reload).to be_running
+ end
+ end
+
def statuses
pipeline.reload.statuses
end
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index 0da0e57dbcd..74a23ed2a3f 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -8,21 +8,19 @@ describe Ci::UpdateBuildQueueService do
context 'when updating specific runners' do
let(:runner) { create(:ci_runner) }
- context 'when there are runner that can pick build' do
+ context 'when there is a runner that can pick build' do
before do
build.project.runners << runner
end
it 'ticks runner queue value' do
- expect { subject.execute(build) }
- .to change { runner.ensure_runner_queue_value }
+ expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
end
end
- context 'when there are no runners that can pick build' do
+ context 'when there is no runner that can pick build' do
it 'does not tick runner queue value' do
- expect { subject.execute(build) }
- .not_to change { runner.ensure_runner_queue_value }
+ expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
end
end
end
@@ -30,21 +28,61 @@ describe Ci::UpdateBuildQueueService do
context 'when updating shared runners' do
let(:runner) { create(:ci_runner, :shared) }
- context 'when there are runner that can pick build' do
+ context 'when there is no runner that can pick build' do
it 'ticks runner queue value' do
- expect { subject.execute(build) }
- .to change { runner.ensure_runner_queue_value }
+ expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
end
end
- context 'when there are no runners that can pick build' do
+ context 'when there is no runner that can pick build due to tag mismatch' do
before do
build.tag_list = [:docker]
end
it 'does not tick runner queue value' do
- expect { subject.execute(build) }
- .not_to change { runner.ensure_runner_queue_value }
+ expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
+ end
+ end
+
+ context 'when there is no runner that can pick build due to being disabled on project' do
+ before do
+ build.project.shared_runners_enabled = false
+ end
+
+ it 'does not tick runner queue value' do
+ expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
+ end
+ end
+ end
+
+ context 'when updating group runners' do
+ let(:group) { create :group }
+ let(:project) { create :project, group: group }
+ let(:runner) { create :ci_runner, groups: [group] }
+
+ context 'when there is a runner that can pick build' do
+ it 'ticks runner queue value' do
+ expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
+ end
+ end
+
+ context 'when there is no runner that can pick build due to tag mismatch' do
+ before do
+ build.tag_list = [:docker]
+ end
+
+ it 'does not tick runner queue value' do
+ expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
+ end
+ end
+
+ context 'when there is no runner that can pick build due to being disabled on project' do
+ before do
+ build.project.group_runners_enabled = false
+ end
+
+ it 'does not tick runner queue value' do
+ expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
end
end
end
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
index 473dbcbb692..bca1e71bef2 100644
--- a/spec/services/clusters/applications/schedule_installation_service_spec.rb
+++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Clusters::Applications::ScheduleInstallationService do
def count_scheduled
- application_class&.with_status(:scheduled)&.count || 0
+ application&.class&.with_status(:scheduled)&.count || 0
end
shared_examples 'a failing service' do
@@ -10,45 +10,42 @@ describe Clusters::Applications::ScheduleInstallationService do
expect(ClusterInstallAppWorker).not_to receive(:perform_async)
count_before = count_scheduled
- expect { service.execute }.to raise_error(StandardError)
+ expect { service.execute(application) }.to raise_error(StandardError)
expect(count_scheduled).to eq(count_before)
end
end
describe '#execute' do
- let(:application_class) { Clusters::Applications::Helm }
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
- let(:service) { described_class.new(project, nil, cluster: cluster, application_class: application_class) }
+ let(:project) { double(:project) }
+ let(:service) { described_class.new(project, nil) }
- it 'creates a new application' do
- allow(ClusterInstallAppWorker).to receive(:perform_async)
+ context 'when application is installable' do
+ let(:application) { create(:clusters_applications_helm, :installable) }
- expect { service.execute }.to change { application_class.count }.by(1)
- end
-
- it 'make the application scheduled' do
- expect(ClusterInstallAppWorker).to receive(:perform_async).with(application_class.application_name, kind_of(Numeric)).once
+ it 'make the application scheduled' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once
- expect { service.execute }.to change { application_class.with_status(:scheduled).count }.by(1)
+ expect { service.execute(application) }.to change { application.class.with_status(:scheduled).count }.by(1)
+ end
end
context 'when installation is already in progress' do
let(:application) { create(:clusters_applications_helm, :installing) }
- let(:cluster) { application.cluster }
it_behaves_like 'a failing service'
end
- context 'when application_class is nil' do
- let(:application_class) { nil }
+ context 'when application is nil' do
+ let(:application) { nil }
it_behaves_like 'a failing service'
end
context 'when application cannot be persisted' do
+ let(:application) { create(:clusters_applications_helm) }
+
before do
- expect_any_instance_of(application_class).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
+ expect(application).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
end
it_behaves_like 'a failing service'
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 1c2f9c5cf43..1685dc748bd 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -8,80 +8,22 @@ describe Clusters::CreateService do
subject { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do
- shared_context 'valid params' do
- 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'
- }
- }
- end
- end
-
- shared_context 'invalid params' do
- 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'
- }
- }
- end
- end
-
- shared_examples 'create cluster' do
- it 'creates a cluster object and performs a worker' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
-
- expect { subject }
- .to change { Clusters::Cluster.count }.by(1)
- .and change { Clusters::Providers::Gcp.count }.by(1)
-
- expect(subject.name).to eq('test-cluster')
- expect(subject.user).to eq(user)
- expect(subject.project).to eq(project)
- expect(subject.provider.gcp_project_id).to eq('gcp-project')
- expect(subject.provider.zone).to eq('us-central1-a')
- expect(subject.provider.num_nodes).to eq(1)
- expect(subject.provider.machine_type).to eq('machine_type-a')
- expect(subject.provider.access_token).to eq(access_token)
- expect(subject.platform).to be_nil
- end
- end
-
- shared_examples 'error' do
- it 'returns an error' do
- expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { subject }.to change { Clusters::Cluster.count }.by(0)
- expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
- end
- end
-
context 'when project has no clusters' do
context 'when correct params' do
- include_context 'valid params'
+ include_context 'valid cluster create params'
- include_examples 'create cluster'
+ include_examples 'create cluster service success'
end
context 'when invalid params' do
- include_context 'invalid params'
+ include_context 'invalid cluster create params'
- include_examples 'error'
+ include_examples 'create cluster service error'
end
end
context 'when project has a cluster' do
- include_context 'valid params'
+ include_context 'valid cluster create params'
let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
it 'does not create a cluster' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 26fdf8d4b24..35826de5814 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -14,6 +14,72 @@ describe GitPushService, services: true do
project.add_master(user)
end
+ describe 'with remote mirrors' do
+ let(:project) { create(:project, :repository, :remote_mirror) }
+
+ subject do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ context 'when remote mirror feature is enabled' do
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'when remote mirror feature is disabled' do
+ before do
+ stub_application_setting(mirror_available: false)
+ end
+
+ context 'with remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = true
+ end
+
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'without remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = false
+ end
+
+ it 'does not fails stuck remote mirrors' do
+ expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'does not updates remote mirrors' do
+ expect(project).not_to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
describe 'Push branches' do
subject do
execute_service(project, user, oldrev, newrev, ref)
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index e8216abb08b..a9baccd061a 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -53,8 +53,8 @@ describe Groups::DestroyService do
end
it 'verifies that paths have been deleted' do
- expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, group.path)).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
end
end
end
@@ -71,13 +71,13 @@ describe Groups::DestroyService do
after do
# Clean up stale directories
- gitlab_shell.rm_namespace(project.repository_storage_path, group.path)
- gitlab_shell.rm_namespace(project.repository_storage_path, remove_path)
+ gitlab_shell.rm_namespace(project.repository_storage, group.path)
+ gitlab_shell.rm_namespace(project.repository_storage, remove_path)
end
it 'verifies original paths and projects still exist' do
- expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, group.path)).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
expect(Project.unscoped.count).to eq(1)
expect(Group.unscoped.count).to eq(2)
end
@@ -144,7 +144,7 @@ describe Groups::DestroyService do
let!(:project) { create(:project, :legacy_storage, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
@@ -152,7 +152,7 @@ describe Groups::DestroyService do
let!(:project) { create(:project, :empty_repo, namespace: group) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
end
diff --git a/spec/services/groups/nested_create_service_spec.rb b/spec/services/groups/nested_create_service_spec.rb
index 6491fb34777..86fdd43c1e5 100644
--- a/spec/services/groups/nested_create_service_spec.rb
+++ b/spec/services/groups/nested_create_service_spec.rb
@@ -59,8 +59,11 @@ describe Groups::NestedCreateService do
describe "#execute" do
it 'returns the group if it already existed' do
- parent = create(:group, path: 'a-group', owner: user)
- child = create(:group, path: 'a-sub-group', parent: parent, owner: user)
+ parent = create(:group, path: 'a-group')
+ child = create(:group, path: 'a-sub-group', parent: parent)
+
+ parent.add_owner(user)
+ child.add_owner(user)
expect(service.execute).to eq(child)
end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index b8fa3e3d124..dcf4503ef9c 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -5,34 +5,6 @@ describe Issuable::CommonSystemNotesService do
let(:project) { create(:project) }
let(:issuable) { create(:issue) }
- shared_examples 'system note creation' do |update_params, note_text|
- subject { described_class.new(project, user).execute(issuable, [])}
-
- before do
- issuable.assign_attributes(update_params)
- issuable.save
- end
-
- it 'creates 1 system note with the correct content' do
- expect { subject }.to change { Note.count }.from(0).to(1)
-
- note = Note.last
- expect(note.note).to match(note_text)
- expect(note.noteable_type).to eq(issuable.class.name)
- end
- end
-
- shared_examples 'WIP notes creation' do |wip_action|
- subject { described_class.new(project, user).execute(issuable, []) }
-
- it 'creates WIP toggle and title change notes' do
- expect { subject }.to change { Note.count }.from(0).to(2)
-
- expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
- expect(Note.second.note).to match('changed title')
- end
- end
-
describe '#execute' do
it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description'
diff --git a/spec/services/keys/destroy_service_spec.rb b/spec/services/keys/destroy_service_spec.rb
new file mode 100644
index 00000000000..28ac72ddd42
--- /dev/null
+++ b/spec/services/keys/destroy_service_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Keys::DestroyService do
+ let(:user) { create(:user) }
+
+ subject { described_class.new(user) }
+
+ it 'destroys a key' do
+ key = create(:key)
+
+ expect { subject.execute(key) }.to change(Key, :count).by(-1)
+ end
+end
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index ae819c011de..80bac590a11 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -8,6 +8,7 @@ describe Labels::TransferService do
let(:group_3) { create(:group) }
let(:project_1) { create(:project, namespace: group_2) }
let(:project_2) { create(:project, namespace: group_3) }
+ let(:project_3) { create(:project, namespace: group_1) }
let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
@@ -23,6 +24,7 @@ describe Labels::TransferService do
create(:labeled_issue, project: project_1, labels: [group_label_4])
create(:labeled_issue, project: project_1, labels: [project_label_1])
create(:labeled_issue, project: project_2, labels: [group_label_5])
+ create(:labeled_issue, project: project_3, labels: [group_label_1])
create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2])
create(:labeled_merge_request, source_project: project_2, labels: [group_label_5])
end
@@ -52,5 +54,13 @@ describe Labels::TransferService do
expect(project_1.labels.where(title: group_label_4.title)).to be_empty
end
+
+ it 'updates only label links in the given project' do
+ service.execute
+
+ targets = LabelLink.where(label_id: group_label_1.id).map(&:target)
+
+ expect(targets).to eq(project_3.issues)
+ end
end
end
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
index 837b8a56d12..d57852615d9 100644
--- a/spec/services/merge_requests/conflicts/list_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe MergeRequests::Conflicts::ListService do
describe '#can_be_resolved_in_ui?' do
def create_merge_request(source_branch)
- create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', merge_status: :unchecked) do |mr|
mr.mark_as_unmergeable
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index c38ddf4612b..e8568bf8bb3 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -219,7 +219,7 @@ describe MergeRequests::MergeService do
service.execute(merge_request)
- expect(merge_request.merge_error).to include(error_message)
+ expect(merge_request.merge_error).to include('Something went wrong during merge')
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
@@ -231,7 +231,7 @@ describe MergeRequests::MergeService do
service.execute(merge_request)
- expect(merge_request.merge_error).to include(error_message)
+ expect(merge_request.merge_error).to include('Something went wrong during merge pre-receive hook')
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index 240aa638f79..8838742a637 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -112,32 +112,6 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
service.trigger(unrelated_pipeline)
end
end
-
- context 'when the merge request is not mergeable' do
- let(:mr_conflict) do
- create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
- source_branch: 'master', target_branch: 'feature-conflict',
- source_project: project, target_project: project)
- end
-
- let(:conflict_pipeline) do
- create(:ci_pipeline, project: project, ref: mr_conflict.source_branch,
- sha: mr_conflict.diff_head_sha, status: 'success',
- head_pipeline_of: mr_conflict)
- end
-
- it 'does not merge the merge request' do
- expect(MergeWorker).not_to receive(:perform_async)
-
- service.trigger(conflict_pipeline)
- end
-
- it 'creates todos for unmergeability' do
- expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(mr_conflict)
-
- service.trigger(conflict_pipeline)
- end
- end
end
describe "#cancel" do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index f5cff66de6d..2b2b983494f 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -57,6 +57,88 @@ describe Notes::CreateService do
end
end
+ context 'note diff file' 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(:line_number) { 14 }
+ let(:position) do
+ Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: line_number,
+ diff_refs: merge_request.diff_refs)
+ end
+ let(:previous_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: project_with_repo)
+ end
+
+ context 'when eligible to have a note diff file' do
+ 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
+
+ it 'note is associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_present
+ end
+ end
+
+ context 'when DiffNote is a reply' do
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: previous_note.discussion_id,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: position.to_h)
+ end
+
+ it 'note is not associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_nil
+ end
+
+ context 'when DiffNote from an image' do
+ let(:image_position) do
+ Gitlab::Diff::Position.new(old_path: "files/images/6049019_460s.jpg",
+ new_path: "files/images/6049019_460s.jpg",
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 100,
+ diff_refs: merge_request.diff_refs,
+ position_type: 'image')
+ end
+
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: nil,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: image_position.to_h)
+ end
+
+ it 'note is not associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_nil
+ end
+ end
+ end
+ end
+
context 'note with commands' do
context 'as a user who can update the target' do
context '/close, /label, /assign & /milestone' do
diff --git a/spec/services/notes/resolve_service_spec.rb b/spec/services/notes/resolve_service_spec.rb
new file mode 100644
index 00000000000..b54d40a7a5c
--- /dev/null
+++ b/spec/services/notes/resolve_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Notes::ResolveService do
+ let(:merge_request) { create(:merge_request) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) }
+ let(:user) { merge_request.author }
+
+ describe '#execute' do
+ it "resolves the note" do
+ described_class.new(merge_request.project, user).execute(note)
+ note.reload
+
+ expect(note.resolved?).to be true
+ expect(note.resolved_by).to eq(user)
+ end
+
+ it "sends notifications if all discussions are resolved" do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+ described_class.new(merge_request.project, user).execute(note)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 55bbe954491..831678b949d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe NotificationService, :mailer do
include EmailSpec::Matchers
+ include NotificationHelpers
let(:notification) { described_class.new }
let(:assignee) { create(:user) }
@@ -13,12 +14,6 @@ describe NotificationService, :mailer do
end
shared_examples 'notifications for new mentions' do
- def send_notifications(*new_mentions)
- mentionable.description = new_mentions.map(&:to_reference).join(' ')
-
- notification.send(notification_method, mentionable, new_mentions, @u_disabled)
- end
-
it 'sends no emails when no new mentions are present' do
send_notifications
should_not_email_anyone
@@ -96,6 +91,37 @@ describe NotificationService, :mailer do
it_should_behave_like 'participating by assignee notification'
end
+ describe '#async' do
+ let(:async) { notification.async }
+ set(:key) { create(:personal_key) }
+
+ it 'returns an Async object with the correct parent' do
+ expect(async).to be_a(described_class::Async)
+ expect(async.parent).to eq(notification)
+ end
+
+ context 'when receiving a public method' do
+ it 'schedules a MailScheduler::NotificationServiceWorker' do
+ expect(MailScheduler::NotificationServiceWorker)
+ .to receive(:perform_async).with('new_key', key)
+
+ async.new_key(key)
+ end
+ end
+
+ context 'when receiving a private method' do
+ it 'raises NoMethodError' do
+ expect { async.notifiable?(key) }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'when recieving a non-existent method' do
+ it 'raises NoMethodError' do
+ expect { async.foo(key) }.to raise_error(NoMethodError)
+ end
+ end
+ end
+
describe 'Keys' do
describe '#new_key' do
let(:key_options) { {} }
@@ -982,6 +1008,8 @@ describe NotificationService, :mailer do
let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
before do
+ project.add_master(merge_request.author)
+ project.add_master(merge_request.assignee)
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
@@ -1093,15 +1121,18 @@ describe NotificationService, :mailer do
end
describe '#reassigned_merge_request' do
+ let(:current_user) { create(:user) }
+
before do
update_custom_notification(:reassign_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:reassign_merge_request, @u_custom_global)
end
it do
- notification.reassigned_merge_request(merge_request, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
should_email(merge_request.assignee)
+ should_email(merge_request.author)
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1116,7 +1147,7 @@ describe NotificationService, :mailer do
end
it 'adds "assigned" reason for new assignee' do
- notification.reassigned_merge_request(merge_request, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
email = find_email_for(merge_request.assignee)
@@ -1126,7 +1157,7 @@ describe NotificationService, :mailer do
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
- let(:notification_trigger) { notification.reassigned_merge_request(merge_request, @u_disabled) }
+ let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, merge_request.author) }
end
end
@@ -1193,6 +1224,32 @@ describe NotificationService, :mailer do
end
end
+ describe '#merge_request_unmergeable' do
+ it "sends email to merge request author" do
+ notification.merge_request_unmergeable(merge_request)
+
+ should_email(merge_request.author)
+ expect(email_recipients.size).to eq(1)
+ end
+
+ describe 'when merge_when_pipeline_succeeds is true' do
+ before do
+ merge_request.update_attributes(
+ merge_when_pipeline_succeeds: true,
+ merge_user: create(:user)
+ )
+ end
+
+ it "sends email to merge request author and merge_user" do
+ notification.merge_request_unmergeable(merge_request)
+
+ should_email(merge_request.author)
+ should_email(merge_request.merge_user)
+ expect(email_recipients.size).to eq(2)
+ end
+ end
+ end
+
describe '#closed_merge_request' do
before do
update_custom_notification(:close_merge_request, @u_guest_custom, resource: project)
@@ -1878,30 +1935,6 @@ describe NotificationService, :mailer do
group
end
- def create_global_setting_for(user, level)
- setting = user.global_notification_setting
- setting.level = level
- setting.save
-
- user
- end
-
- def create_user_with_notification(level, username, resource = project)
- user = create(:user, username: username)
- setting = user.notification_settings_for(resource)
- setting.level = level
- setting.save
-
- user
- end
-
- # Create custom notifications
- # When resource is nil it means global notification
- def update_custom_notification(event, user, resource: nil, value: true)
- setting = user.notification_settings_for(resource)
- setting.update!(event => value)
- end
-
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb
index 609d678caea..9aa9237d875 100644
--- a/spec/services/projects/create_from_template_service_spec.rb
+++ b/spec/services/projects/create_from_template_service_spec.rb
@@ -7,7 +7,7 @@ describe Projects::CreateFromTemplateService do
path: user.to_param,
template_name: 'rails',
description: 'project description',
- visibility_level: Gitlab::VisibilityLevel::PRIVATE
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC
}
end
@@ -23,8 +23,24 @@ describe Projects::CreateFromTemplateService do
project = subject.execute
expect(project).to be_saved
- expect(project.scheduled?).to be(true)
- expect(project.description).to match('project description')
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(project.import_scheduled?).to be(true)
+ end
+
+ context 'the result project' do
+ before do
+ Sidekiq::Testing.inline! do
+ @project = subject.execute
+ end
+
+ @project.reload
+ end
+
+ it 'overrides template description' do
+ expect(@project.description).to match('project description')
+ end
+
+ it 'overrides template visibility_level' do
+ expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index e35f0f6337a..a8f003b1073 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -171,7 +171,6 @@ describe Projects::CreateService, '#execute' do
context 'when another repository already exists on disk' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:opts) do
{
@@ -186,7 +185,7 @@ describe Projects::CreateService, '#execute' do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ gitlab_shell.remove_repository(repository_storage, "#{user.namespace.full_path}/existing")
end
it 'does not allow to create a project when path matches existing repository on disk' do
@@ -222,7 +221,7 @@ describe Projects::CreateService, '#execute' do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, hashed_path)
+ gitlab_shell.remove_repository(repository_storage, hashed_path)
end
it 'does not allow to create a project when path matches existing repository on disk' do
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index a66e3c5e995..b63f409579e 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -18,8 +18,8 @@ describe Projects::DestroyService do
it 'deletes the project' do
expect(Project.unscoped.all).not_to include(project)
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path + '.git')).to be_falsey
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path + '.git')).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, path + '.git')).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path + '.git')).to be_falsey
end
end
@@ -65,6 +65,19 @@ describe Projects::DestroyService do
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
+ context 'when has remote mirrors' do
+ let!(:project) do
+ create(:project, :repository, namespace: user.namespace).tap do |project|
+ project.remote_mirrors.create(url: 'http://test.com')
+ end
+ end
+ let!(:async) { true }
+
+ it 'destroys them' do
+ expect(RemoteMirror.count).to eq(0)
+ end
+ end
+
it_behaves_like 'deleting the project'
it 'invalidates personal_project_count cache' do
@@ -252,21 +265,21 @@ describe Projects::DestroyService do
let(:path) { project.disk_path + '.git' }
before do
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_truthy
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
# Dont run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_falsey
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy
+ expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_truthy
end
it 'restores the repositories' do
Sidekiq::Testing.fake! { described_class.new(project, user).attempt_repositories_rollback }
- expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
- expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
+ expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_truthy
+ expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 0f7c46367d0..a93f6f1ddc2 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -112,7 +112,7 @@ describe Projects::ForkService do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ gitlab_shell.remove_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
end
it 'does not allow creation' do
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 747bd4529a0..7dca81eb59e 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -16,8 +16,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do
it 'renames project and wiki repositories' do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy
end
it 'updates project to be hashed and not read-only' do
@@ -52,8 +52,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do
service.execute
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey
expect(project.repository_read_only?).to be_falsey
end
@@ -63,11 +63,11 @@ describe Projects::HashedStorage::MigrateRepositoryService do
before do
hashed_storage.ensure_storage_path_exists
- gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end
it 'does not try to move nil repository over hashed' do
- expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name)
+ expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, from_name, to_name)
expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki")
service.execute
@@ -76,7 +76,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do
end
def expect_move_repository(from_name, to_name)
- expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original
+ expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage, from_name, to_name).and_call_original
end
end
end
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
index f964f9972cd..06b470849b3 100644
--- a/spec/services/projects/open_issues_count_service_spec.rb
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -2,20 +2,53 @@ require 'spec_helper'
describe Projects::OpenIssuesCountService do
describe '#count' do
- it 'returns the number of open issues' do
- project = create(:project)
- create(:issue, :opened, project: project)
+ let(:project) { create(:project) }
- expect(described_class.new(project).count).to eq(1)
+ context 'when user is nil' do
+ it 'does not include confidential issues in the issue count' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ expect(described_class.new(project).count).to eq(1)
+ end
end
- it 'does not include confidential issues in the issue count' do
- project = create(:project)
+ context 'when user is provided' do
+ let(:user) { create(:user) }
+
+ context 'when user can read confidential issues' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'returns the right count with confidential issues' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ expect(described_class.new(project, user).count).to eq(2)
+ end
+
+ it 'uses total_open_issues_count cache key' do
+ expect(described_class.new(project, user).cache_key_name).to eq('total_open_issues_count')
+ end
+ end
+
+ context 'when user cannot read confidential issues' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'does not include confidential issues' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
- create(:issue, :opened, project: project)
- create(:issue, :opened, confidential: true, project: project)
+ expect(described_class.new(project, user).count).to eq(1)
+ end
- expect(described_class.new(project).count).to eq(1)
+ it 'uses public_open_issues_count cache key' do
+ expect(described_class.new(project, user).cache_key_name).to eq('public_open_issues_count')
+ end
+ end
end
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index ff9b2372a35..3e6483d7e28 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -84,7 +84,7 @@ describe Projects::TransferService do
end
def project_path(project)
- File.join(project.repository_storage_path, "#{project.disk_path}.git")
+ project.repository.path_to_repo
end
def current_path
@@ -94,7 +94,7 @@ describe Projects::TransferService do
it 'rolls back repo location' do
attempt_project_transfer
- expect(Dir.exist?(original_path)).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be(true)
expect(original_path).to eq current_path
end
@@ -165,7 +165,7 @@ describe Projects::TransferService do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{group.full_path}/#{project.path}")
+ gitlab_shell.remove_repository(repository_storage, "#{group.full_path}/#{project.path}")
end
it { expect(@result).to eq false }
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 1b6caeab15d..347ac13828c 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -29,25 +29,10 @@ describe Projects::UpdatePagesService do
end
describe 'pages artifacts' do
- context 'with expiry date' do
- before do
- build.artifacts_expire_in = "2 days"
- build.save!
- end
-
- it "doesn't delete artifacts" do
- expect(execute).to eq(:success)
-
- expect(build.reload.artifacts?).to eq(true)
- end
- end
-
- context 'without expiry date' do
- it "does delete artifacts" do
- expect(execute).to eq(:success)
+ it "doesn't delete artifacts after deploying" do
+ expect(execute).to eq(:success)
- expect(build.reload.artifacts?).to eq(false)
- end
+ expect(build.reload.artifacts?).to eq(true)
end
end
@@ -100,25 +85,10 @@ describe Projects::UpdatePagesService do
end
describe 'pages artifacts' do
- context 'with expiry date' do
- before do
- build.artifacts_expire_in = "2 days"
- build.save!
- end
-
- it "doesn't delete artifacts" do
- expect(execute).to eq(:success)
-
- expect(build.artifacts?).to eq(true)
- end
- end
-
- context 'without expiry date' do
- it "does delete artifacts" do
- expect(execute).to eq(:success)
+ it "doesn't delete artifacts after deploying" do
+ expect(execute).to eq(:success)
- expect(build.reload.artifacts?).to eq(false)
- end
+ expect(build.artifacts?).to eq(true)
end
end
@@ -153,11 +123,13 @@ describe Projects::UpdatePagesService do
expect(execute).not_to eq(:success)
end
- it 'fails for empty file fails' do
- build.job_artifacts_archive.update_attributes(file: empty_file)
+ context 'when using empty file' do
+ let(:file) { empty_file }
- expect { execute }
- .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
+ it 'fails to extract' do
+ expect { execute }
+ .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
+ end
end
context 'when timeout happens by DNS error' do
@@ -171,13 +143,12 @@ describe Projects::UpdatePagesService do
build.reload
expect(deploy_status).to be_failed
- expect(build.artifacts?).to be_truthy
end
end
context 'when failed to extract zip artifacts' do
before do
- allow_any_instance_of(described_class)
+ expect_any_instance_of(described_class)
.to receive(:extract_zip_archive!)
.and_raise(Projects::UpdatePagesService::FailedToExtractError)
end
@@ -188,21 +159,19 @@ describe Projects::UpdatePagesService do
build.reload
expect(deploy_status).to be_failed
- expect(build.artifacts?).to be_truthy
end
end
context 'when missing artifacts metadata' do
before do
- allow(build).to receive(:artifacts_metadata?).and_return(false)
+ expect(build).to receive(:artifacts_metadata?).and_return(false)
end
- it 'does not raise an error and remove artifacts as failed job' do
+ it 'does not raise an error as failed job' do
execute
build.reload
expect(deploy_status).to be_failed
- expect(build.artifacts?).to be_falsey
end
end
end
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
new file mode 100644
index 00000000000..723cb374c37
--- /dev/null
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -0,0 +1,359 @@
+require 'spec_helper'
+
+describe Projects::UpdateRemoteMirrorService do
+ let(:project) { create(:project, :repository) }
+ let(:remote_project) { create(:forked_project_with_submodules) }
+ let(:repository) { project.repository }
+ let(:raw_repository) { repository.raw }
+ let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo, enabled: true, only_protected_branches: false) }
+
+ subject { described_class.new(project, project.creator) }
+
+ describe "#execute", :skip_gitaly_mock do
+ before do
+ create_branch(repository, 'existing-branch')
+ allow(raw_repository).to receive(:remote_tags) do
+ generate_tags(repository, 'v1.0.0', 'v1.1.0')
+ end
+ allow(raw_repository).to receive(:push_remote_branches).and_return(true)
+ end
+
+ it "fetches the remote repository" do
+ expect(repository).to receive(:fetch_remote).with(remote_mirror.remote_name, no_tags: true) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ end
+
+ subject.execute(remote_mirror)
+ end
+
+ it "succeeds" do
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+
+ result = subject.execute(remote_mirror)
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ describe 'Syncing branches' do
+ it "push all the branches the first time" do
+ allow(repository).to receive(:fetch_remote)
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, local_branch_names)
+
+ subject.execute(remote_mirror)
+ end
+
+ it "does not push anything is remote is up to date" do
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+
+ expect(raw_repository).not_to receive(:push_remote_branches)
+
+ subject.execute(remote_mirror)
+ end
+
+ it "sync new branches" do
+ # call local_branch_names early so it is not called after the new branch has been created
+ current_branches = local_branch_names
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, current_branches) }
+ create_branch(repository, 'my-new-branch')
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['my-new-branch'])
+
+ subject.execute(remote_mirror)
+ end
+
+ it "sync updated branches" do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_branch(repository, 'existing-branch')
+ end
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+
+ subject.execute(remote_mirror)
+ end
+
+ context 'when push only protected branches option is set' do
+ let(:unprotected_branch_name) { 'existing-branch' }
+ let(:protected_branch_name) do
+ project.repository.branch_names.find { |n| n != unprotected_branch_name }
+ end
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: protected_branch_name)
+ end
+
+ before do
+ project.reload
+ remote_mirror.only_protected_branches = true
+ end
+
+ it "sync updated protected branches" do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_branch(repository, protected_branch_name)
+ end
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+
+ it 'does not sync unprotected branches' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_branch(repository, unprotected_branch_name)
+ end
+
+ expect(raw_repository).not_to receive(:push_remote_branches).with(remote_mirror.remote_name, [unprotected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when branch exists in local and remote repo' do
+ context 'when it has diverged' do
+ it 'syncs branches' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ update_remote_branch(repository, remote_mirror.remote_name, 'markdown')
+ end
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['markdown'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+
+ describe 'for delete' do
+ context 'when branch exists in local and remote repo' do
+ it 'deletes the branch from remote repo' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ delete_branch(repository, 'existing-branch')
+ end
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when push only protected branches option is set' do
+ before do
+ remote_mirror.only_protected_branches = true
+ end
+
+ context 'when branch exists in local and remote repo' do
+ let!(:protected_branch_name) { local_branch_names.first }
+
+ before do
+ create(:protected_branch, project: project, name: protected_branch_name)
+ project.reload
+ end
+
+ it 'deletes the protected branch from remote repo' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ delete_branch(repository, protected_branch_name)
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+
+ it 'does not delete the unprotected branch from remote repo' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+ delete_branch(repository, 'existing-branch')
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when branch only exists on remote repo' do
+ let!(:protected_branch_name) { 'remote-branch' }
+
+ before do
+ create(:protected_branch, project: project, name: protected_branch_name)
+ end
+
+ context 'when it has diverged' do
+ it 'does not delete the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ rev = repository.find_branch('markdown').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches)
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when it has not diverged' do
+ it 'deletes the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, protected_branch_name, masterrev.id)
+ end
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+ end
+
+ context 'when branch only exists on remote repo' do
+ context 'when it has diverged' do
+ it 'does not delete the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ rev = repository.find_branch('markdown').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
+ end
+
+ expect(raw_repository).not_to receive(:delete_remote_branches)
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when it has not diverged' do
+ it 'deletes the remote branch' do
+ allow(repository).to receive(:fetch_remote) do
+ sync_remote(repository, remote_mirror.remote_name, local_branch_names)
+
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', masterrev.id)
+ end
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['remote-branch'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Syncing tags' do
+ before do
+ allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+ end
+
+ context 'when there are not tags to push' do
+ it 'does not try to push tags' do
+ allow(repository).to receive(:remote_tags) { {} }
+ allow(repository).to receive(:tags) { [] }
+
+ expect(repository).not_to receive(:push_tags)
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when there are some tags to push' do
+ it 'pushes tags to remote' do
+ allow(raw_repository).to receive(:remote_tags) { {} }
+
+ expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['v1.0.0', 'v1.1.0'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+
+ context 'when there are some tags to delete' do
+ it 'deletes tags from remote' do
+ remote_tags = generate_tags(repository, 'v1.0.0', 'v1.1.0')
+ allow(raw_repository).to receive(:remote_tags) { remote_tags }
+
+ repository.rm_tag(create(:user), 'v1.0.0')
+
+ expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['v1.0.0'])
+
+ subject.execute(remote_mirror)
+ end
+ end
+ end
+ end
+
+ def create_branch(repository, branch_name)
+ rugged = repository.rugged
+ masterrev = repository.find_branch('master').dereferenced_target
+ parentrev = repository.commit(masterrev).parent_id
+
+ rugged.references.create("refs/heads/#{branch_name}", parentrev)
+
+ repository.expire_branches_cache
+ end
+
+ def create_remote_branch(repository, remote_name, branch_name, source_id)
+ rugged = repository.rugged
+
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_id)
+ end
+
+ def sync_remote(repository, remote_name, local_branch_names)
+ rugged = repository.rugged
+
+ local_branch_names.each do |branch|
+ target = repository.find_branch(branch).try(:dereferenced_target)
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch}", target.id) if target
+ end
+ end
+
+ def update_remote_branch(repository, remote_name, branch)
+ rugged = repository.rugged
+ masterrev = repository.find_branch('master').dereferenced_target.id
+
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
+ repository.expire_branches_cache
+ end
+
+ def update_branch(repository, branch)
+ rugged = repository.rugged
+ masterrev = repository.find_branch('master').dereferenced_target.id
+
+ # Updated existing branch
+ rugged.references.create("refs/heads/#{branch}", masterrev, force: true)
+ repository.expire_branches_cache
+ end
+
+ def delete_branch(repository, branch)
+ rugged = repository.rugged
+
+ rugged.references.delete("refs/heads/#{branch}")
+ repository.expire_branches_cache
+ end
+
+ def generate_tags(repository, *tag_names)
+ tag_names.each_with_object([]) do |name, tags|
+ tag = repository.find_tag(name)
+ target = tag.try(:target)
+ target_commit = tag.try(:dereferenced_target)
+ tags << Gitlab::Git::Tag.new(repository.raw_repository, {
+ name: name,
+ target: target,
+ target_commit: target_commit
+ })
+ end
+ end
+
+ def local_branch_names
+ branch_names = repository.branches.map(&:name)
+ # we want the protected branch to be pushed first
+ branch_names.unshift(branch_names.delete('master'))
+ end
+end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index f48d466d263..3e6073b9861 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -200,7 +200,7 @@ describe Projects::UpdateService do
end
after do
- gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing")
+ gitlab_shell.remove_repository(repository_storage, "#{user.namespace.full_path}/existing")
end
it 'does not allow renaming when new path matches existing repository on disk' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index f793f55e51b..bd835a1fca6 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -306,6 +306,23 @@ describe QuickActions::InterpretService do
end
end
+ shared_examples 'copy_metadata command' do
+ it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do
+ source_issuable # populate the issue
+ todo_label # populate this label
+ inreview_label # populate this label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates[:add_label_ids]).to match_array([inreview_label.id, todo_label.id])
+
+ if source_issuable.milestone
+ expect(updates[:milestone_id]).to eq(source_issuable.milestone.id)
+ else
+ expect(updates).not_to have_key(:milestone_id)
+ end
+ end
+ end
+
shared_examples 'shrug command' do
it 'appends ¯\_(ツ)_/¯ to the comment' do
new_content, _ = service.execute(content, issuable)
@@ -757,6 +774,65 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ context '/copy_metadata command' do
+ let(:todo_label) { create(:label, project: project, title: 'To Do') }
+ let(:inreview_label) { create(:label, project: project, title: 'In Review') }
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/copy_metadata' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference}" }
+ let(:issuable) { issue }
+ end
+
+ context 'when the parent issuable has a milestone' do
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [todo_label, inreview_label], milestone: milestone) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'when more than one issuable is passed' do
+ it_behaves_like 'copy_metadata command' do
+ let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
+ let(:other_label) { create(:label, project: project, title: 'Other') }
+ let(:other_source_issuable) { create(:labeled_issue, project: project, labels: [other_label]) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference} #{other_source_issuable.to_reference}" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'cross project references' do
+ it_behaves_like 'empty command' do
+ let(:other_project) { create(:project, :public) }
+ let(:source_issuable) { create(:labeled_issue, project: other_project, labels: [todo_label, inreview_label]) }
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/copy_metadata imaginary#1234" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:other_project) { create(:project, :private) }
+ let(:source_issuable) { create(:issue, project: other_project) }
+
+ let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+ end
+ end
+
context '/duplicate command' do
it_behaves_like 'duplicate command' do
let(:issue_duplicate) { create(:issue, project: project) }
diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb
index 2d7fa3f80f7..ab1c638fc39 100644
--- a/spec/services/repository_archive_clean_up_service_spec.rb
+++ b/spec/services/repository_archive_clean_up_service_spec.rb
@@ -1,15 +1,47 @@
require 'spec_helper'
describe RepositoryArchiveCleanUpService do
- describe '#execute' do
- subject(:service) { described_class.new }
+ subject(:service) { described_class.new }
+ describe '#execute (new archive locations)' do
+ let(:sha) { "0" * 40 }
+
+ it 'removes outdated archives and directories in a new-style path' do
+ in_directory_with_files("project-999/#{sha}", %w[tar tar.bz2 tar.gz zip], 3.hours) do |dirname, files|
+ service.execute
+
+ files.each { |filename| expect(File.exist?(filename)).to be_falsy }
+ expect(File.directory?(dirname)).to be_falsy
+ expect(File.directory?(File.dirname(dirname))).to be_falsy
+ end
+ end
+
+ it 'does not remove directories when they contain outdated non-archives' do
+ in_directory_with_files("project-999/#{sha}", %w[tar conf rb], 2.hours) do |dirname, files|
+ service.execute
+
+ expect(File.directory?(dirname)).to be_truthy
+ end
+ end
+
+ it 'does not remove in-date archives in a new-style path' do
+ in_directory_with_files("project-999/#{sha}", %w[tar tar.bz2 tar.gz zip], 1.hour) do |dirname, files|
+ service.execute
+
+ files.each { |filename| expect(File.exist?(filename)).to be_truthy }
+ end
+ end
+ end
+
+ describe '#execute (legacy archive locations)' do
context 'when the downloads directory does not exist' do
it 'does not remove any archives' do
path = '/invalid/path/'
stub_repository_downloads_path(path)
+ allow(File).to receive(:directory?).and_call_original
expect(File).to receive(:directory?).with(path).and_return(false)
+
expect(service).not_to receive(:clean_up_old_archives)
expect(service).not_to receive(:clean_up_empty_directories)
@@ -19,7 +51,7 @@ describe RepositoryArchiveCleanUpService do
context 'when the downloads directory exists' do
shared_examples 'invalid archive files' do |dirname, extensions, mtime|
- it 'does not remove files and directoy' do
+ it 'does not remove files and directory' do
in_directory_with_files(dirname, extensions, mtime) do |dir, files|
service.execute
@@ -43,7 +75,7 @@ describe RepositoryArchiveCleanUpService do
end
context 'with files older than 2 hours inside invalid directories' do
- it_behaves_like 'invalid archive files', 'john_doe/sample.git', %w[conf rb tar tar.gz], 2.hours
+ it_behaves_like 'invalid archive files', 'john/doe/sample.git', %w[conf rb tar tar.gz], 2.hours
end
context 'with files newer than 2 hours that matches valid archive extensions' do
@@ -58,24 +90,24 @@ describe RepositoryArchiveCleanUpService do
it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb tar tar.gz], 1.hour
end
end
+ end
- def in_directory_with_files(dirname, extensions, mtime)
- Dir.mktmpdir do |tmpdir|
- stub_repository_downloads_path(tmpdir)
- dir = File.join(tmpdir, dirname)
- files = create_temporary_files(dir, extensions, mtime)
+ def in_directory_with_files(dirname, extensions, mtime)
+ Dir.mktmpdir do |tmpdir|
+ stub_repository_downloads_path(tmpdir)
+ dir = File.join(tmpdir, dirname)
+ files = create_temporary_files(dir, extensions, mtime)
- yield(dir, files)
- end
+ yield(dir, files)
end
+ end
- def stub_repository_downloads_path(path)
- allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path)
- end
+ def stub_repository_downloads_path(path)
+ allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path)
+ end
- def create_temporary_files(dir, extensions, mtime)
- FileUtils.mkdir_p(dir)
- FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime)
- end
+ def create_temporary_files(dir, extensions, mtime)
+ FileUtils.mkdir_p(dir)
+ FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime)
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 893804f1470..e28b0ea5cf2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -909,13 +909,7 @@ describe SystemNoteService do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h,"
- end
-
- it 'appends a comma to separate the note from the update_at time' do
- noteable.update_attribute(:time_estimate, 277200)
-
- expect(subject.note).to end_with(',')
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
end
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 28dfa9cf59c..962b9f40c4f 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -170,6 +170,7 @@ describe TestHooks::ProjectService do
end
context 'wiki_page_events' do
+ let(:project) { create(:project, :wiki_repo) }
let(:trigger) { 'wiki_page_events' }
let(:trigger_key) { :wiki_page_hooks }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 562b89e6767..9a51c873b30 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -721,17 +721,18 @@ describe TodoService do
end
describe '#merge_request_build_failed' do
- it 'creates a pending todo for the merge request author' do
- service.merge_request_build_failed(mr_unassigned)
+ let(:merge_participants) { [mr_unassigned.author, admin] }
- should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ before do
+ allow(mr_unassigned).to receive(:merge_participants).and_return(merge_participants)
end
- it 'creates a pending todo for merge_user' do
- mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
+ it 'creates a pending todo for each merge_participant' do
service.merge_request_build_failed(mr_unassigned)
- should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ merge_participants.each do |participant|
+ should_create_todo(user: participant, author: participant, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ end
end
end
@@ -747,11 +748,19 @@ describe TodoService do
end
describe '#merge_request_became_unmergeable' do
- it 'creates a pending todo for a merge_user' do
+ let(:merge_participants) { [admin, create(:user)] }
+
+ before do
+ allow(mr_unassigned).to receive(:merge_participants).and_return(merge_participants)
+ end
+
+ it 'creates a pending todo for each merge_participant' do
mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
service.merge_request_became_unmergeable(mr_unassigned)
- should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
+ merge_participants.each do |participant|
+ should_create_todo(user: participant, author: participant, target: mr_unassigned, action: Todo::UNMERGEABLE)
+ end
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 11c75ddfcf8..76f1e625fda 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -176,7 +176,7 @@ describe Users::DestroyService do
let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
@@ -184,7 +184,7 @@ describe Users::DestroyService do
let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
end
end
end
diff --git a/spec/services/users/respond_to_terms_service_spec.rb b/spec/services/users/respond_to_terms_service_spec.rb
new file mode 100644
index 00000000000..fb08dd10b87
--- /dev/null
+++ b/spec/services/users/respond_to_terms_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Users::RespondToTermsService do
+ let(:user) { create(:user) }
+ let(:term) { create(:term) }
+
+ subject(:service) { described_class.new(user, term) }
+
+ describe '#execute' do
+ it 'creates a new agreement if it did not exist' do
+ expect { service.execute(accepted: true) }
+ .to change { user.term_agreements.size }.by(1)
+ end
+
+ it 'updates an agreement if it existed' do
+ agreement = create(:term_agreement, user: user, term: term, accepted: true)
+
+ service.execute(accepted: true)
+
+ expect(agreement.reload.accepted).to be_truthy
+ end
+
+ it 'adds the accepted terms to the user' do
+ service.execute(accepted: true)
+
+ expect(user.reload.accepted_term).to eq(term)
+ end
+
+ it 'removes accepted terms when declining' do
+ user.update!(accepted_term: term)
+
+ service.execute(accepted: false)
+
+ expect(user.reload.accepted_term).to be_nil
+ end
+ end
+end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 2ef2e61babc..7995f2c9ae7 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -67,7 +67,7 @@ describe WebHookService do
end
it 'handles exceptions' do
- exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout]
+ exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError]
exceptions.each do |exception_class|
exception = exception_class.new('Exception message')
diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
index b270194d9b8..259f445247e 100644
--- a/spec/services/wiki_pages/create_service_spec.rb
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe WikiPages::CreateService do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :wiki_repo) }
let(:user) { create(:user) }
let(:opts) do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 83664bae046..d3de2331244 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -32,42 +32,19 @@ require 'rainbow/ext/string'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
+# Requires helpers, and shared contexts/examples first since they're used in other support files
+Dir[Rails.root.join("spec/support/helpers/*.rb")].each { |f| require f }
+Dir[Rails.root.join("spec/support/shared_contexts/*.rb")].each { |f| require f }
+Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.use_instantiated_fixtures = false
- config.mock_with :rspec
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::Test::ControllerHelpers, type: :controller
- config.include Devise::Test::ControllerHelpers, type: :view
- config.include Devise::Test::IntegrationHelpers, type: :feature
- config.include Warden::Test::Helpers, type: :request
- config.include LoginHelpers, type: :feature
- config.include SearchHelpers, type: :feature
- config.include CookieHelper, :js
- config.include InputHelper, :js
- config.include SelectionHelper, :js
- config.include InspectRequests, :js
- config.include WaitForRequests, :js
- config.include LiveDebugger, :js
- config.include StubConfiguration
- config.include EmailHelpers, :mailer, type: :mailer
- config.include TestEnv
- config.include ActiveJob::TestHelper
- config.include ActiveSupport::Testing::TimeHelpers
- config.include StubGitlabCalls
- config.include StubGitlabData
- config.include ApiHelpers, :api
- config.include Gitlab::Routing, type: :routing
- config.include MigrationsHelpers, :migration
- config.include StubFeatureFlags
- config.include StubENV
- config.include ExpectOffense
-
config.infer_spec_type_from_file_location!
config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
@@ -82,7 +59,34 @@ RSpec.configure do |config|
metadata[:type] = match[1].singularize.to_sym if match
end
- config.raise_errors_for_deprecations!
+ config.include ActiveJob::TestHelper
+ config.include ActiveSupport::Testing::TimeHelpers
+ config.include CycleAnalyticsHelpers
+ config.include ExpectOffense
+ config.include FactoryBot::Syntax::Methods
+ config.include FixtureHelpers
+ config.include GitlabRoutingHelper
+ config.include StubFeatureFlags
+ config.include StubGitlabCalls
+ config.include StubGitlabData
+ config.include TestEnv
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::IntegrationHelpers, type: :feature
+ config.include LoginHelpers, type: :feature
+ config.include SearchHelpers, type: :feature
+ config.include EmailHelpers, :mailer, type: :mailer
+ config.include Warden::Test::Helpers, type: :request
+ config.include Gitlab::Routing, type: :routing
+ config.include Devise::Test::ControllerHelpers, type: :view
+ config.include ApiHelpers, :api
+ config.include CookieHelper, :js
+ config.include InputHelper, :js
+ config.include SelectionHelper, :js
+ config.include InspectRequests, :js
+ config.include WaitForRequests, :js
+ config.include LiveDebugger, :js
+ config.include MigrationsHelpers, :migration
+ config.include RedisHelpers
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
@@ -110,10 +114,10 @@ RSpec.configure do |config|
m.call(*args)
shard_name, repository_relative_path = args
- shard_path = Gitlab.config.repositories.storages.fetch(shard_name).legacy_disk_path
# We can't leave the hooks in place after a fork, as those would fail in tests
# The "internal" API is not available
- FileUtils.rm_rf(File.join(shard_path, repository_relative_path, 'hooks'))
+ Gitlab::Shell.new.rm_directory(shard_name,
+ File.join(repository_relative_path, 'hooks'))
end
# Enable all features by default for testing
@@ -133,6 +137,13 @@ RSpec.configure do |config|
reset_delivered_emails!
end
+ config.before(:example, :prometheus) do
+ matching_files = File.join(::Prometheus::Client.configuration.multiprocess_files_dir, "*.db")
+ Dir[matching_files].map { |filename| File.delete(filename) if File.file?(filename) }
+
+ Gitlab::Metrics.reset_registry!
+ end
+
config.around(:each, :use_clean_rails_memory_store_caching) do |example|
caching_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
@@ -143,21 +154,27 @@ RSpec.configure do |config|
end
config.around(:each, :clean_gitlab_redis_cache) do |example|
- Gitlab::Redis::Cache.with(&:flushall)
+ redis_cache_cleanup!
example.run
- Gitlab::Redis::Cache.with(&:flushall)
+ redis_cache_cleanup!
end
config.around(:each, :clean_gitlab_redis_shared_state) do |example|
- Gitlab::Redis::SharedState.with(&:flushall)
- Sidekiq.redis(&:flushall)
+ redis_shared_state_cleanup!
+
+ example.run
+
+ redis_shared_state_cleanup!
+ end
+
+ config.around(:each, :clean_gitlab_redis_queues) do |example|
+ redis_queues_cleanup!
example.run
- Gitlab::Redis::SharedState.with(&:flushall)
- Sidekiq.redis(&:flushall)
+ redis_queues_cleanup!
end
# The :each scope runs "inside" the example, so this hook ensures the DB is in the
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index 0388f110d71..a15189db35f 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -196,6 +196,12 @@ shared_examples_for 'group and project milestones' do |route_definition|
expect(json_response['state']).to eq('closed')
end
+
+ it 'updates milestone with only start date' do
+ put api(resource_route, user), start_date: Date.tomorrow
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
describe "GET #{route_definition}/:milestone_id/issues" do
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index dd3089d22e5..52e1bc55191 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -70,8 +70,12 @@ shared_examples 'time tracking endpoints' do |issuable_name|
end
it "add spent time for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
- duration: '2h'
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
+ duration: '2h'
+ end.to change { issuable.reload.updated_at }
+ end
expect(response).to have_gitlab_http_status(201)
expect(json_response['human_total_time_spent']).to eq('2h')
@@ -79,7 +83,11 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'when subtracting time' do
it 'subtracts time of the total spent time' do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ end.to change { issuable.reload.updated_at }
+ end
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1h'
@@ -93,8 +101,12 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'does not modify the total time spent' do
issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
- duration: '-1w'
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
+ duration: '-1w'
+ end.not_to change { issuable.reload.updated_at }
+ end
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
@@ -110,7 +122,11 @@ shared_examples 'time tracking endpoints' do |issuable_name|
end
it "resets spent time for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
+ Timecop.travel(1.minute.from_now) do
+ expect do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user)
+ end.to change { issuable.reload.updated_at }
+ end
expect(response).to have_gitlab_http_status(200)
expect(json_response['total_time_spent']).to eq(0)
diff --git a/spec/support/board_helpers.rb b/spec/support/board_helpers.rb
deleted file mode 100644
index 507d0432d7f..00000000000
--- a/spec/support/board_helpers.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module BoardHelpers
- def click_card(card)
- within card do
- first('.card-number').click
- end
-
- wait_for_sidebar
- end
-
- def wait_for_sidebar
- # loop until the CSS transition is complete
- Timeout.timeout(0.5) do
- loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
- end
- end
-end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 9ddcc5f2fbf..c0ceb0f6605 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -60,6 +60,8 @@ Capybara::Screenshot.register_driver(:chrome) do |driver, path|
end
RSpec.configure do |config|
+ config.include CapybaraHelpers, type: :feature
+
config.before(:context, :js) do
next if $capybara_server_already_started
diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb
deleted file mode 100644
index 868233416bf..00000000000
--- a/spec/support/capybara_helpers.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module CapybaraHelpers
- # Execute a block a certain number of times before considering it a failure
- #
- # The given block is called, and if it raises a `Capybara::ExpectationNotMet`
- # error, we wait `interval` seconds and then try again, until `retries` is
- # met.
- #
- # This allows for better handling of timing-sensitive expectations in a
- # sketchy CI environment, for example.
- #
- # interval - Delay between retries in seconds (default: 0.5)
- # retries - Number of times to execute before failing (default: 5)
- def allowing_for_delay(interval: 0.5, retries: 5)
- tries = 0
-
- begin
- sleep interval
-
- yield
- rescue Capybara::ExpectationNotMet => ex
- if tries <= retries
- tries += 1
- sleep interval
- retry
- else
- raise ex
- end
- end
- end
-
- # Refresh the page. Calling `visit current_url` doesn't seem to work consistently.
- #
- def refresh
- url = current_url
- visit 'about:blank'
- visit url
- end
-
- # Simulate a browser restart by clearing the session cookie.
- def clear_browser_session
- page.driver.browser.manage.delete_cookie('_gitlab_session')
- end
-end
-
-RSpec.configure do |config|
- config.include CapybaraHelpers, type: :feature
-end
diff --git a/spec/support/chunked_io/chunked_io_helpers.rb b/spec/support/chunked_io/chunked_io_helpers.rb
new file mode 100644
index 00000000000..fec1f951563
--- /dev/null
+++ b/spec/support/chunked_io/chunked_io_helpers.rb
@@ -0,0 +1,11 @@
+module ChunkedIOHelpers
+ def sample_trace_raw
+ @sample_trace_raw ||= File.read(expand_fixture_path('trace/sample_trace'))
+ .force_encoding(Encoding::BINARY)
+ end
+
+ def stub_buffer_size(size)
+ stub_const('Ci::BuildTraceChunk::CHUNK_SIZE', size)
+ stub_const('Gitlab::Ci::Trace::ChunkedIO::CHUNK_SIZE', size)
+ end
+end
diff --git a/spec/support/commit_trailers_spec_helper.rb b/spec/support/commit_trailers_spec_helper.rb
index add359946db..efa317fd2f9 100644
--- a/spec/support/commit_trailers_spec_helper.rb
+++ b/spec/support/commit_trailers_spec_helper.rb
@@ -8,7 +8,7 @@ module CommitTrailersSpecHelper
expect(wrapper.attribute('data-user').value).to eq user.id.to_s
end
- def expect_to_have_mailto_link(doc, email:, trailer:)
+ def expect_to_have_mailto_link_with_avatar(doc, email:, trailer:)
wrapper = find_user_wrapper(doc, trailer)
expect_to_have_links_with_url_and_avatar(wrapper, "mailto:#{CGI.escape_html(email)}", email)
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index 3321f920666..368439aa5b0 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -56,7 +56,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end
it "assigns variables" do
- project = create(:project, import_type: provider, creator_id: user.id)
+ project = create(:project, import_type: provider, namespace: user.namespace)
stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
get :status
@@ -69,7 +69,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end
it "does not show already added project" do
- project = create(:project, import_type: provider, creator_id: user.id, import_source: 'asd/vim')
+ project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim')
stub_client(repos: [repo], orgs: [])
get :status
@@ -257,11 +257,12 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do
- let(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' }
before do
+ parent_namespace.add_owner(user)
nested_namespace.add_owner(user)
end
@@ -307,7 +308,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' }
- let!(:parent_namespace) { create(:group, name: 'foo', owner: user) }
+ let!(:parent_namespace) { create(:group, name: 'foo') }
before do
parent_namespace.add_owner(user)
diff --git a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
new file mode 100644
index 00000000000..72912ffb89d
--- /dev/null
+++ b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+shared_context 'Ldap::OmniauthCallbacksController' do
+ include LoginHelpers
+ include LdapHelpers
+
+ let(:uid) { 'my-uid' }
+ let(:provider) { 'ldapmain' }
+ let(:valid_login?) { true }
+ let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider) }
+ let(:ldap_server_config) do
+ { main: ldap_config_defaults(:main) }
+ end
+
+ def ldap_config_defaults(key, hash = {})
+ {
+ provider_name: "ldap#{key}",
+ attributes: {},
+ encryption: 'plain'
+ }.merge(hash)
+ end
+
+ before do
+ stub_ldap_setting(enabled: true, servers: ldap_server_config)
+ described_class.define_providers!
+ Rails.application.reload_routes!
+
+ mock_auth_hash(provider.to_s, uid, user.email)
+ stub_omniauth_provider(provider, context: request)
+
+ allow(Gitlab::Auth::LDAP::Access).to receive(:allowed?).and_return(valid_login?)
+ end
+end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
deleted file mode 100644
index 73cc64c0b74..00000000000
--- a/spec/support/cycle_analytics_helpers.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-module CycleAnalyticsHelpers
- def create_commit_referencing_issue(issue, branch_name: generate(:branch))
- project.repository.add_branch(user, branch_name, 'master')
- create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
- end
-
- def create_commit(message, project, user, branch_name, count: 1)
- repository = project.repository
- oldrev = repository.commit(branch_name).sha
-
- if Timecop.frozen? && Gitlab::GitalyClient.feature_enabled?(:operation_user_commit_files)
- mock_gitaly_multi_action_dates(repository.raw)
- end
-
- commit_shas = Array.new(count) do |index|
- commit_sha = repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name)
- repository.commit(commit_sha)
-
- commit_sha
- end
-
- GitPushService.new(project,
- user,
- oldrev: oldrev,
- newrev: commit_shas.last,
- ref: 'refs/heads/master').execute
- end
-
- def create_cycle(user, project, issue, mr, milestone, pipeline)
- issue.update(milestone: milestone)
- pipeline.run
-
- ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
-
- merge_merge_requests_closing_issue(user, project, issue)
- ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
-
- ci_build
- end
-
- def create_merge_request_closing_issue(user, project, issue, message: nil, source_branch: nil, commit_message: 'commit message')
- if !source_branch || project.repository.commit(source_branch).blank?
- source_branch = generate(:branch)
- project.repository.add_branch(user, source_branch, 'master')
- end
-
- sha = project.repository.create_file(
- user,
- generate(:branch),
- 'content',
- message: commit_message,
- branch_name: source_branch)
- project.repository.commit(sha)
-
- opts = {
- title: 'Awesome merge_request',
- description: message || "Fixes #{issue.to_reference}",
- source_branch: source_branch,
- target_branch: 'master'
- }
-
- mr = MergeRequests::CreateService.new(project, user, opts).execute
- NewMergeRequestWorker.new.perform(mr, user)
- mr
- end
-
- def merge_merge_requests_closing_issue(user, project, issue)
- merge_requests = issue.closed_by_merge_requests(user)
-
- merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
- end
-
- def deploy_master(user, project, environment: 'production')
- dummy_job =
- case environment
- when 'production'
- dummy_production_job(user, project)
- when 'staging'
- dummy_staging_job(user, project)
- else
- raise ArgumentError
- end
-
- CreateDeploymentService.new(dummy_job).execute
- end
-
- def dummy_production_job(user, project)
- new_dummy_job(user, project, 'production')
- end
-
- def dummy_staging_job(user, project)
- new_dummy_job(user, project, 'staging')
- end
-
- def dummy_pipeline(project)
- Ci::Pipeline.new(
- sha: project.repository.commit('master').sha,
- ref: 'master',
- source: :push,
- project: project,
- protected: false)
- end
-
- def new_dummy_job(user, project, environment)
- project.environments.find_or_create_by(name: environment)
-
- Ci::Build.new(
- project: project,
- user: user,
- environment: environment,
- ref: 'master',
- tag: false,
- name: 'dummy',
- stage: 'dummy',
- pipeline: dummy_pipeline(project),
- protected: false)
- end
-
- def mock_gitaly_multi_action_dates(raw_repository)
- allow(raw_repository).to receive(:multi_action).and_wrap_original do |m, *args|
- new_date = Time.now
- branch_update = m.call(*args)
-
- if branch_update.newrev
- _, opts = args
- commit = raw_repository.commit(branch_update.newrev).rugged_commit
- branch_update.newrev = commit.amend(
- update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}",
- author: commit.author.merge(time: new_date),
- committer: commit.committer.merge(time: new_date)
- )
- end
-
- branch_update
- end
- end
-end
-
-RSpec.configure do |config|
- config.include CycleAnalyticsHelpers
-end
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
deleted file mode 100644
index c7890e49c66..00000000000
--- a/spec/support/factory_bot.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-RSpec.configure do |config|
- config.include FactoryBot::Syntax::Methods
-end
diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb
deleted file mode 100644
index 8854382dc6b..00000000000
--- a/spec/support/fixture_helpers.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module FixtureHelpers
- def fixture_file(filename, dir: '')
- return '' if filename.blank?
-
- File.read(expand_fixture_path(filename, dir: dir))
- end
-
- def expand_fixture_path(filename, dir: '')
- File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
- end
-end
-
-RSpec.configure do |config|
- config.include FixtureHelpers
-end
diff --git a/spec/support/forgery_protection.rb b/spec/support/forgery_protection.rb
index a5e7b761651..fa87d5fa881 100644
--- a/spec/support/forgery_protection.rb
+++ b/spec/support/forgery_protection.rb
@@ -1,11 +1,18 @@
+module ForgeryProtection
+ def with_forgery_protection
+ ActionController::Base.allow_forgery_protection = true
+ yield
+ ensure
+ ActionController::Base.allow_forgery_protection = false
+ end
+
+ module_function :with_forgery_protection
+end
+
RSpec.configure do |config|
config.around(:each, :allow_forgery_protection) do |example|
- begin
- ActionController::Base.allow_forgery_protection = true
-
+ ForgeryProtection.with_forgery_protection do
example.call
- ensure
- ActionController::Base.allow_forgery_protection = false
end
end
end
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index 876b3b8242d..44b3de23b99 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -8,7 +8,7 @@
#
# Usage:
#
-# ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb
+# ./spec/support/generate-seed-repo-rb > spec/support/helpers/seed_repo.rb
#
#
diff --git a/spec/support/gitlab-git-test.git/README.md b/spec/support/gitlab-git-test.git/README.md
index f072cd421be..f757e613ee6 100644
--- a/spec/support/gitlab-git-test.git/README.md
+++ b/spec/support/gitlab-git-test.git/README.md
@@ -12,5 +12,5 @@ inflate the size of the gitlab-ce repository.
- make changes in your local clone of gitlab-git-test
- run `git push` which will push to your local source `gitlab-ce/spec/support/gitlab-git-test.git`
- in gitlab-ce: run `spec/support/prepare-gitlab-git-test-for-commit`
-- in gitlab-ce: `git add spec/support/seed_repo.rb spec/support/gitlab-git-test.git`
+- in gitlab-ce: `git add spec/support/helpers/seed_repo.rb spec/support/gitlab-git-test.git`
- commit your changes in gitlab-ce
diff --git a/spec/support/gitlab_verify.rb b/spec/support/gitlab_verify.rb
deleted file mode 100644
index 13e2e37624d..00000000000
--- a/spec/support/gitlab_verify.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do
- describe 'batching' do
- let(:first_batch) { objects[0].id..objects[0].id }
- let(:second_batch) { objects[1].id..objects[1].id }
- let(:third_batch) { objects[2].id..objects[2].id }
-
- it 'iterates through objects in batches' do
- expect(collect_ranges).to eq([first_batch, second_batch, third_batch])
- end
-
- it 'allows the starting ID to be specified' do
- expect(collect_ranges(start: second_batch.first)).to eq([second_batch, third_batch])
- end
-
- it 'allows the finishing ID to be specified' do
- expect(collect_ranges(finish: second_batch.last)).to eq([first_batch, second_batch])
- end
- end
-end
-
-module GitlabVerifyHelpers
- def collect_ranges(args = {})
- verifier = described_class.new(args.merge(batch_size: 1))
-
- collect_results(verifier).map { |range, _| range }
- end
-
- def collect_failures
- verifier = described_class.new(batch_size: 1)
-
- out = {}
-
- collect_results(verifier).map { |_, failures| out.merge!(failures) }
-
- out
- end
-
- def collect_results(verifier)
- out = []
-
- verifier.run_batches { |*args| out << args }
-
- out
- end
-end
diff --git a/spec/support/api_helpers.rb b/spec/support/helpers/api_helpers.rb
index ac0c7a9b493..ac0c7a9b493 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/helpers/api_helpers.rb
diff --git a/spec/support/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb
index 3f4a4243cb6..3f4a4243cb6 100644
--- a/spec/support/bare_repo_operations.rb
+++ b/spec/support/helpers/bare_repo_operations.rb
diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb
new file mode 100644
index 00000000000..b85fde222ea
--- /dev/null
+++ b/spec/support/helpers/board_helpers.rb
@@ -0,0 +1,16 @@
+module BoardHelpers
+ def click_card(card)
+ within card do
+ first('.board-card-number').click
+ end
+
+ wait_for_sidebar
+ end
+
+ def wait_for_sidebar
+ # loop until the CSS transition is complete
+ Timeout.timeout(0.5) do
+ loop until evaluate_script('$(".right-sidebar").outerWidth()') == 290
+ end
+ end
+end
diff --git a/spec/support/helpers/capybara_helpers.rb b/spec/support/helpers/capybara_helpers.rb
new file mode 100644
index 00000000000..bcc2df44708
--- /dev/null
+++ b/spec/support/helpers/capybara_helpers.rb
@@ -0,0 +1,43 @@
+module CapybaraHelpers
+ # Execute a block a certain number of times before considering it a failure
+ #
+ # The given block is called, and if it raises a `Capybara::ExpectationNotMet`
+ # error, we wait `interval` seconds and then try again, until `retries` is
+ # met.
+ #
+ # This allows for better handling of timing-sensitive expectations in a
+ # sketchy CI environment, for example.
+ #
+ # interval - Delay between retries in seconds (default: 0.5)
+ # retries - Number of times to execute before failing (default: 5)
+ def allowing_for_delay(interval: 0.5, retries: 5)
+ tries = 0
+
+ begin
+ sleep interval
+
+ yield
+ rescue Capybara::ExpectationNotMet => ex
+ if tries <= retries
+ tries += 1
+ sleep interval
+ retry
+ else
+ raise ex
+ end
+ end
+ end
+
+ # Refresh the page. Calling `visit current_url` doesn't seem to work consistently.
+ #
+ def refresh
+ url = current_url
+ visit 'about:blank'
+ visit url
+ end
+
+ # Simulate a browser restart by clearing the session cookie.
+ def clear_browser_session
+ page.driver.browser.manage.delete_cookie('_gitlab_session')
+ end
+end
diff --git a/spec/support/cookie_helper.rb b/spec/support/helpers/cookie_helper.rb
index 5ff7b0b68c9..5ff7b0b68c9 100644
--- a/spec/support/cookie_helper.rb
+++ b/spec/support/helpers/cookie_helper.rb
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
new file mode 100644
index 00000000000..55359d36597
--- /dev/null
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -0,0 +1,137 @@
+module CycleAnalyticsHelpers
+ def create_commit_referencing_issue(issue, branch_name: generate(:branch))
+ project.repository.add_branch(user, branch_name, 'master')
+ create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+ end
+
+ def create_commit(message, project, user, branch_name, count: 1)
+ repository = project.repository
+ oldrev = repository.commit(branch_name).sha
+
+ if Timecop.frozen? && Gitlab::GitalyClient.feature_enabled?(:operation_user_commit_files)
+ mock_gitaly_multi_action_dates(repository.raw)
+ end
+
+ commit_shas = Array.new(count) do |index|
+ commit_sha = repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name)
+ repository.commit(commit_sha)
+
+ commit_sha
+ end
+
+ GitPushService.new(project,
+ user,
+ oldrev: oldrev,
+ newrev: commit_shas.last,
+ ref: 'refs/heads/master').execute
+ end
+
+ def create_cycle(user, project, issue, mr, milestone, pipeline)
+ issue.update(milestone: milestone)
+ pipeline.run
+
+ ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ merge_merge_requests_closing_issue(user, project, issue)
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
+
+ ci_build
+ end
+
+ def create_merge_request_closing_issue(user, project, issue, message: nil, source_branch: nil, commit_message: 'commit message')
+ if !source_branch || project.repository.commit(source_branch).blank?
+ source_branch = generate(:branch)
+ project.repository.add_branch(user, source_branch, 'master')
+ end
+
+ sha = project.repository.create_file(
+ user,
+ generate(:branch),
+ 'content',
+ message: commit_message,
+ branch_name: source_branch)
+ project.repository.commit(sha)
+
+ opts = {
+ title: 'Awesome merge_request',
+ description: message || "Fixes #{issue.to_reference}",
+ source_branch: source_branch,
+ target_branch: 'master'
+ }
+
+ mr = MergeRequests::CreateService.new(project, user, opts).execute
+ NewMergeRequestWorker.new.perform(mr, user)
+ mr
+ end
+
+ def merge_merge_requests_closing_issue(user, project, issue)
+ merge_requests = issue.closed_by_merge_requests(user)
+
+ merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
+ end
+
+ def deploy_master(user, project, environment: 'production')
+ dummy_job =
+ case environment
+ when 'production'
+ dummy_production_job(user, project)
+ when 'staging'
+ dummy_staging_job(user, project)
+ else
+ raise ArgumentError
+ end
+
+ CreateDeploymentService.new(dummy_job).execute
+ end
+
+ def dummy_production_job(user, project)
+ new_dummy_job(user, project, 'production')
+ end
+
+ def dummy_staging_job(user, project)
+ new_dummy_job(user, project, 'staging')
+ end
+
+ def dummy_pipeline(project)
+ Ci::Pipeline.new(
+ sha: project.repository.commit('master').sha,
+ ref: 'master',
+ source: :push,
+ project: project,
+ protected: false)
+ end
+
+ def new_dummy_job(user, project, environment)
+ project.environments.find_or_create_by(name: environment)
+
+ Ci::Build.new(
+ project: project,
+ user: user,
+ environment: environment,
+ ref: 'master',
+ tag: false,
+ name: 'dummy',
+ stage: 'dummy',
+ pipeline: dummy_pipeline(project),
+ protected: false)
+ end
+
+ def mock_gitaly_multi_action_dates(raw_repository)
+ allow(raw_repository).to receive(:multi_action).and_wrap_original do |m, *args|
+ new_date = Time.now
+ branch_update = m.call(*args)
+
+ if branch_update.newrev
+ _, opts = args
+ commit = raw_repository.commit(branch_update.newrev).rugged_commit
+ branch_update.newrev = commit.amend(
+ update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}",
+ author: commit.author.merge(time: new_date),
+ committer: commit.committer.merge(time: new_date)
+ )
+ end
+
+ branch_update
+ end
+ end
+end
diff --git a/spec/support/database_connection_helpers.rb b/spec/support/helpers/database_connection_helpers.rb
index 763329499f0..763329499f0 100644
--- a/spec/support/database_connection_helpers.rb
+++ b/spec/support/helpers/database_connection_helpers.rb
diff --git a/spec/support/devise_helpers.rb b/spec/support/helpers/devise_helpers.rb
index 66874e10f38..66874e10f38 100644
--- a/spec/support/devise_helpers.rb
+++ b/spec/support/helpers/devise_helpers.rb
diff --git a/spec/support/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb
index ae149631ed9..ae149631ed9 100644
--- a/spec/support/drag_to_helper.rb
+++ b/spec/support/helpers/drag_to_helper.rb
diff --git a/spec/support/dropzone_helper.rb b/spec/support/helpers/dropzone_helper.rb
index fe72d320fcf..fe72d320fcf 100644
--- a/spec/support/dropzone_helper.rb
+++ b/spec/support/helpers/dropzone_helper.rb
diff --git a/spec/support/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 1fb8252459f..1fb8252459f 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
diff --git a/spec/support/fake_migration_classes.rb b/spec/support/helpers/fake_migration_classes.rb
index b0fc8422857..b0fc8422857 100644
--- a/spec/support/fake_migration_classes.rb
+++ b/spec/support/helpers/fake_migration_classes.rb
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb
index a7605cd483a..a7605cd483a 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/helpers/fake_u2f_device.rb
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index 50457b64745..ad0053ec5cf 100644
--- a/spec/support/helpers/features/sorting_helpers.rb
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -15,7 +15,7 @@ module Spec
def sort_by(value)
find('button.dropdown-toggle').click
- page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link(value)
end
end
diff --git a/spec/support/filter_item_select_helper.rb b/spec/support/helpers/filter_item_select_helper.rb
index 519e84d359e..519e84d359e 100644
--- a/spec/support/filter_item_select_helper.rb
+++ b/spec/support/helpers/filter_item_select_helper.rb
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
index 721d359c2ee..721d359c2ee 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/helpers/filter_spec_helper.rb
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 5f42ff77fb2..5f42ff77fb2 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
diff --git a/spec/support/helpers/fixture_helpers.rb b/spec/support/helpers/fixture_helpers.rb
new file mode 100644
index 00000000000..611d19f36a0
--- /dev/null
+++ b/spec/support/helpers/fixture_helpers.rb
@@ -0,0 +1,11 @@
+module FixtureHelpers
+ def fixture_file(filename, dir: '')
+ return '' if filename.blank?
+
+ File.read(expand_fixture_path(filename, dir: dir))
+ end
+
+ def expand_fixture_path(filename, dir: '')
+ File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
+ end
+end
diff --git a/spec/support/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb
index b8289e6c5f1..b8289e6c5f1 100644
--- a/spec/support/git_http_helpers.rb
+++ b/spec/support/helpers/git_http_helpers.rb
diff --git a/spec/support/helpers/gitlab_verify_helpers.rb b/spec/support/helpers/gitlab_verify_helpers.rb
new file mode 100644
index 00000000000..5df4bf24ec2
--- /dev/null
+++ b/spec/support/helpers/gitlab_verify_helpers.rb
@@ -0,0 +1,25 @@
+module GitlabVerifyHelpers
+ def collect_ranges(args = {})
+ verifier = described_class.new(args.merge(batch_size: 1))
+
+ collect_results(verifier).map { |range, _| range }
+ end
+
+ def collect_failures
+ verifier = described_class.new(batch_size: 1)
+
+ out = {}
+
+ collect_results(verifier).map { |_, failures| out.merge!(failures) }
+
+ out
+ end
+
+ def collect_results(verifier)
+ out = []
+
+ verifier.run_batches { |*args| out << args }
+
+ out
+ end
+end
diff --git a/spec/support/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index 3f7279a50e0..3f7279a50e0 100644
--- a/spec/support/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
diff --git a/spec/support/import_spec_helper.rb b/spec/support/helpers/import_spec_helper.rb
index d4eced724fa..d4eced724fa 100644
--- a/spec/support/import_spec_helper.rb
+++ b/spec/support/helpers/import_spec_helper.rb
diff --git a/spec/support/input_helper.rb b/spec/support/helpers/input_helper.rb
index acbb42274ec..acbb42274ec 100644
--- a/spec/support/input_helper.rb
+++ b/spec/support/helpers/input_helper.rb
diff --git a/spec/support/inspect_requests.rb b/spec/support/helpers/inspect_requests.rb
index 88ddc5c7f6c..88ddc5c7f6c 100644
--- a/spec/support/inspect_requests.rb
+++ b/spec/support/helpers/inspect_requests.rb
diff --git a/spec/support/issue_helpers.rb b/spec/support/helpers/issue_helpers.rb
index ffd72515f37..ffd72515f37 100644
--- a/spec/support/issue_helpers.rb
+++ b/spec/support/helpers/issue_helpers.rb
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
new file mode 100644
index 00000000000..086a345dca8
--- /dev/null
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -0,0 +1,66 @@
+require 'action_dispatch/testing/test_request'
+require 'fileutils'
+
+module JavaScriptFixturesHelpers
+ include Gitlab::Popen
+
+ FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
+
+ # Public: Removes all fixture files from given directory
+ #
+ # directory_name - directory of the fixtures (relative to FIXTURE_PATH)
+ #
+ def clean_frontend_fixtures(directory_name)
+ directory_name = File.expand_path(directory_name, FIXTURE_PATH)
+ Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name|
+ FileUtils.rm(file_name)
+ end
+ end
+
+ # Public: Store a response object as fixture file
+ #
+ # response - string or response object to store
+ # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
+ #
+ def store_frontend_fixture(response, fixture_file_name)
+ fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
+ fixture = response.respond_to?(:body) ? parse_response(response) : response
+
+ FileUtils.mkdir_p(File.dirname(fixture_file_name))
+ File.write(fixture_file_name, fixture)
+ end
+
+ def remove_repository(project)
+ Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
+ end
+
+ private
+
+ # Private: Prepare a response object for use as a frontend fixture
+ #
+ # response - response object to prepare
+ #
+ def parse_response(response)
+ fixture = response.body
+ fixture.force_encoding("utf-8")
+
+ response_mime_type = Mime::Type.lookup(response.content_type)
+ if response_mime_type.html?
+ doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
+
+ link_tags = doc.css('link')
+ link_tags.remove
+
+ scripts = doc.css("script:not([type='text/template']):not([type='text/x-template'])")
+ scripts.remove
+
+ fixture = doc.to_html
+
+ # replace relative links
+ test_host = ActionDispatch::TestRequest::DEFAULT_ENV['HTTP_HOST']
+ fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
+ end
+
+ fixture
+ end
+end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index 88a7aeba461..88a7aeba461 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
new file mode 100644
index 00000000000..683a64504a1
--- /dev/null
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -0,0 +1,167 @@
+module KubernetesHelpers
+ include Gitlab::Kubernetes
+
+ def kube_response(body)
+ { body: body.to_json }
+ end
+
+ def kube_pods_response
+ kube_response(kube_pods_body)
+ end
+
+ def kube_deployments_response
+ kube_response(kube_deployments_body)
+ end
+
+ def stub_kubeclient_discover(api_url)
+ 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))
+ end
+
+ def stub_kubeclient_pods(response = nil)
+ stub_kubeclient_discover(service.api_url)
+ pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
+
+ WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
+ end
+
+ def stub_kubeclient_deployments(response = nil)
+ stub_kubeclient_discover(service.api_url)
+ deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{service.actual_namespace}/deployments"
+
+ WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
+ end
+
+ def stub_kubeclient_get_secrets(api_url, **options)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(kube_response(kube_v1_secrets_body(options)))
+ end
+
+ def stub_kubeclient_get_secrets_error(api_url)
+ WebMock.stub_request(:get, api_url + '/api/v1/secrets')
+ .to_return(status: [404, "Internal Server Error"])
+ end
+
+ def kube_v1_secrets_body(**options)
+ {
+ "kind" => "SecretList",
+ "apiVersion": "v1",
+ "items" => [
+ {
+ "metadata": {
+ "name": options[:metadata_name] || "default-token-1",
+ "namespace": "kube-system"
+ },
+ "data": {
+ "token": options[:token] || Base64.encode64('token-sample-123')
+ }
+ }
+ ]
+ }
+ end
+
+ def kube_v1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
+ ]
+ }
+ end
+
+ def kube_v1beta1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
+ { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
+ ]
+ }
+ end
+
+ def kube_pods_body
+ {
+ "kind" => "PodList",
+ "items" => [kube_pod]
+ }
+ end
+
+ def kube_deployments_body
+ {
+ "kind" => "DeploymentList",
+ "items" => [kube_deployment]
+ }
+ end
+
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil)
+ {
+ "metadata" => {
+ "name" => name,
+ "generate_name" => "generated-name-with-suffix",
+ "creationTimestamp" => "2016-11-25T19:55:19Z",
+ "labels" => {
+ "app" => app,
+ "track" => track
+ }
+ },
+ "spec" => {
+ "containers" => [
+ { "name" => "container-0" },
+ { "name" => "container-1" }
+ ]
+ },
+ "status" => { "phase" => status }
+ }
+ end
+
+ def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil)
+ {
+ "metadata" => {
+ "name" => name,
+ "generation" => 4,
+ "labels" => {
+ "app" => app,
+ "track" => track
+ }.compact
+ },
+ "spec" => { "replicas" => 3 },
+ "status" => {
+ "observedGeneration" => 4,
+ "replicas" => 3,
+ "updatedReplicas" => 3,
+ "availableReplicas" => 3
+ }
+ }
+ end
+
+ def kube_terminals(service, pod)
+ pod_name = pod['metadata']['name']
+ containers = pod['spec']['containers']
+
+ containers.map do |container|
+ terminal = {
+ selectors: { pod: pod_name, container: container['name'] },
+ url: container_exec_url(service.api_url, service.actual_namespace, pod_name, container['name']),
+ subprotocols: ['channel.k8s.io'],
+ headers: { 'Authorization' => ["Bearer #{service.token}"] },
+ created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
+ max_session_time: 0
+ }
+ terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
+ terminal
+ end
+ end
+
+ def kube_deployment_rollout_status
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments(kube_deployment)
+ end
+
+ def empty_deployment_rollout_status
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments()
+ end
+end
diff --git a/spec/support/helpers/ldap_helpers.rb b/spec/support/helpers/ldap_helpers.rb
new file mode 100644
index 00000000000..b90bbc4b106
--- /dev/null
+++ b/spec/support/helpers/ldap_helpers.rb
@@ -0,0 +1,53 @@
+module LdapHelpers
+ def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap))
+ ::Gitlab::Auth::LDAP::Adapter.new(provider, ldap)
+ end
+
+ def user_dn(uid)
+ "uid=#{uid},ou=users,dc=example,dc=com"
+ end
+
+ # Accepts a hash of Gitlab::Auth::LDAP::Config keys and values.
+ #
+ # Example:
+ # stub_ldap_config(
+ # group_base: 'ou=groups,dc=example,dc=com',
+ # admin_group: 'my-admin-group'
+ # )
+ def stub_ldap_config(messages)
+ allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages)
+ end
+
+ def stub_ldap_setting(messages)
+ allow(Gitlab.config.ldap).to receive_messages(to_settings(messages))
+ end
+
+ # Stub an LDAP person search and provide the return entry. Specify `nil` for
+ # `entry` to simulate when an LDAP person is not found
+ #
+ # Example:
+ # adapter = ::Gitlab::Auth::LDAP::Adapter.new('ldapmain', double(:ldap))
+ # ldap_user_entry = ldap_user_entry('john_doe')
+ #
+ # stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter)
+ def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain')
+ return_value = ::Gitlab::Auth::LDAP::Person.new(entry, provider) if entry.present?
+
+ allow(::Gitlab::Auth::LDAP::Person)
+ .to receive(:find_by_uid).with(uid, any_args).and_return(return_value)
+ end
+
+ # Create a simple LDAP user entry.
+ def ldap_user_entry(uid)
+ entry = Net::LDAP::Entry.new
+ entry['dn'] = user_dn(uid)
+ entry['uid'] = uid
+
+ entry
+ end
+
+ def raise_ldap_connection_error
+ allow_any_instance_of(Gitlab::Auth::LDAP::Adapter)
+ .to receive(:ldap_search).and_raise(Gitlab::Auth::LDAP::LDAPConnectionError)
+ end
+end
diff --git a/spec/support/live_debugger.rb b/spec/support/helpers/live_debugger.rb
index 911eb48a8ca..911eb48a8ca 100644
--- a/spec/support/live_debugger.rb
+++ b/spec/support/helpers/live_debugger.rb
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
new file mode 100644
index 00000000000..f7b71bf42e3
--- /dev/null
+++ b/spec/support/helpers/login_helpers.rb
@@ -0,0 +1,170 @@
+require_relative 'devise_helpers'
+
+module LoginHelpers
+ include DeviseHelpers
+
+ # Overriding Devise::Test::IntegrationHelpers#sign_in to store @current_user
+ # since we may need it in LiveDebugger#live_debug.
+ def sign_in(resource, scope: nil)
+ super
+
+ @current_user = resource
+ end
+
+ # Overriding Devise::Test::IntegrationHelpers#sign_out to clear @current_user.
+ def sign_out(resource_or_scope)
+ super
+
+ @current_user = nil
+ end
+
+ # Internal: Log in as a specific user or a new user of a specific role
+ #
+ # user_or_role - User object, or a role to create (e.g., :admin, :user)
+ #
+ # Examples:
+ #
+ # # Create a user automatically
+ # gitlab_sign_in(:user)
+ #
+ # # Create an admin automatically
+ # gitlab_sign_in(:admin)
+ #
+ # # Provide an existing User record
+ # user = create(:user)
+ # gitlab_sign_in(user)
+ def gitlab_sign_in(user_or_role, **kwargs)
+ user =
+ if user_or_role.is_a?(User)
+ user_or_role
+ else
+ create(user_or_role)
+ end
+
+ gitlab_sign_in_with(user, **kwargs)
+
+ @current_user = user
+ end
+
+ def gitlab_sign_in_via(provider, user, uid)
+ mock_auth_hash(provider, uid, user.email)
+ visit new_user_session_path
+ click_link provider
+ end
+
+ # Requires Javascript driver.
+ def gitlab_sign_out
+ find(".header-user-dropdown-toggle").click
+ click_link "Sign out"
+ @current_user = nil
+
+ expect(page).to have_button('Sign in')
+ end
+
+ private
+
+ # Private: Login as the specified user
+ #
+ # user - User instance to login with
+ # remember - Whether or not to check "Remember me" (default: false)
+ def gitlab_sign_in_with(user, remember: false)
+ visit new_user_session_path
+
+ fill_in "user_login", with: user.email
+ fill_in "user_password", with: "12345678"
+ check 'user_remember_me' if remember
+
+ click_button "Sign in"
+ end
+
+ def login_via(provider, user, uid, remember_me: false)
+ mock_auth_hash(provider, uid, user.email)
+ visit new_user_session_path
+ expect(page).to have_content('Sign in with')
+
+ check 'remember_me' if remember_me
+
+ click_link "oauth-login-#{provider}"
+ end
+
+ def mock_auth_hash(provider, uid, email)
+ # The mock_auth configuration allows you to set per-provider (or default)
+ # authentication hashes to return during integration testing.
+ OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
+ provider: provider,
+ uid: uid,
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ },
+ credentials: {
+ token: 'mock_token',
+ secret: 'mock_secret'
+ },
+ extra: {
+ raw_info: {
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ }
+ }
+ }
+ })
+ Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
+ end
+
+ def mock_saml_config
+ OpenStruct.new(name: 'saml', label: 'saml', args: {
+ assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
+ idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52',
+ idp_sso_target_url: 'https://idp.example.com/sso/saml',
+ issuer: 'https://localhost:3443/',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ })
+ end
+
+ def stub_omniauth_provider(provider, context: Rails.application)
+ env = env_from_context(context)
+
+ set_devise_mapping(context: context)
+ env['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
+ end
+
+ def stub_omniauth_failure(strategy, message_key, exception = nil)
+ env = @request.env
+
+ env['omniauth.error'] = exception
+ env['omniauth.error.type'] = message_key.to_sym
+ env['omniauth.error.strategy'] = strategy
+ end
+
+ def stub_omniauth_saml_config(messages)
+ set_devise_mapping(context: Rails.application)
+ Rails.application.routes.disable_clear_and_finalize = true
+ Rails.application.routes.draw do
+ post '/users/auth/saml' => 'omniauth_callbacks#saml'
+ end
+ allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
+ stub_omniauth_setting(messages)
+ stub_saml_authorize_path_helpers
+ end
+
+ def stub_saml_authorize_path_helpers
+ allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml')
+ allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
+ end
+
+ def stub_omniauth_config(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(messages)
+ end
+
+ def stub_basic_saml_config
+ allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
+ end
+
+ def stub_saml_group_config(groups)
+ allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
+ end
+end
diff --git a/spec/support/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb
index 39e94ad53de..39e94ad53de 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/helpers/markdown_feature.rb
diff --git a/spec/support/merge_request_helpers.rb b/spec/support/helpers/merge_request_helpers.rb
index 772adff4626..772adff4626 100644
--- a/spec/support/merge_request_helpers.rb
+++ b/spec/support/helpers/merge_request_helpers.rb
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
new file mode 100644
index 00000000000..84abec75c26
--- /dev/null
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -0,0 +1,102 @@
+module MigrationsHelpers
+ def table(name)
+ Class.new(ActiveRecord::Base) do
+ self.table_name = name
+ self.inheritance_column = :_type_disabled
+ end
+ end
+
+ def migrations_paths
+ ActiveRecord::Migrator.migrations_paths
+ end
+
+ def table_exists?(name)
+ ActiveRecord::Base.connection.table_exists?(name)
+ end
+
+ def migrations
+ ActiveRecord::Migrator.migrations(migrations_paths)
+ end
+
+ def clear_schema_cache!
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ conn.schema_cache.clear!
+ end
+ end
+
+ def foreign_key_exists?(source, target = nil, column: nil)
+ ActiveRecord::Base.connection.foreign_keys(source).any? do |key|
+ if column
+ key.options[:column].to_s == column.to_s
+ else
+ key.to_table.to_s == target.to_s
+ end
+ end
+ end
+
+ def reset_column_in_all_models
+ clear_schema_cache!
+
+ # 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))
+
+ # Without that, we get errors because of missing attributes, e.g.
+ # super: no superclass method `elasticsearch_indexing' for #<ApplicationSetting:0x00007f85628508d8>
+ ApplicationSetting.define_attribute_methods
+ end
+
+ def reset_column_information(klass)
+ klass.reset_column_information
+ end
+
+ def previous_migration
+ migrations.each_cons(2) do |previous, migration|
+ break previous if migration.name == described_class.name
+ end
+ end
+
+ def migration_schema_version
+ metadata_schema = self.class.metadata[:schema]
+
+ if metadata_schema == :latest
+ migrations.last.version
+ else
+ metadata_schema || previous_migration.version
+ end
+ end
+
+ def schema_migrate_down!
+ disable_migrations_output do
+ ActiveRecord::Migrator.migrate(migrations_paths,
+ migration_schema_version)
+ end
+
+ reset_column_in_all_models
+ end
+
+ def schema_migrate_up!
+ reset_column_in_all_models
+
+ disable_migrations_output do
+ ActiveRecord::Migrator.migrate(migrations_paths)
+ end
+
+ reset_column_in_all_models
+ end
+
+ def disable_migrations_output
+ ActiveRecord::Migration.verbose = false
+
+ yield
+ ensure
+ ActiveRecord::Migration.verbose = true
+ end
+
+ def migrate!
+ ActiveRecord::Migrator.up(migrations_paths) do |migration|
+ migration.name == described_class.name
+ end
+ end
+end
diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb
new file mode 100644
index 00000000000..9dc1f1de436
--- /dev/null
+++ b/spec/support/helpers/mobile_helpers.rb
@@ -0,0 +1,17 @@
+module MobileHelpers
+ def resize_screen_xs
+ resize_window(575, 768)
+ end
+
+ def resize_screen_sm
+ resize_window(767, 768)
+ end
+
+ def restore_window_size
+ resize_window(1366, 768)
+ end
+
+ def resize_window(width, height)
+ Capybara.current_session.current_window.resize_to(width, height)
+ end
+end
diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb
new file mode 100644
index 00000000000..8d84510fb73
--- /dev/null
+++ b/spec/support/helpers/notification_helpers.rb
@@ -0,0 +1,33 @@
+module NotificationHelpers
+ extend self
+
+ def send_notifications(*new_mentions)
+ mentionable.description = new_mentions.map(&:to_reference).join(' ')
+
+ notification.send(notification_method, mentionable, new_mentions, @u_disabled)
+ end
+
+ def create_global_setting_for(user, level)
+ setting = user.global_notification_setting
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ def create_user_with_notification(level, username, resource = project)
+ user = create(:user, username: username)
+ setting = user.notification_settings_for(resource)
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ # Create custom notifications
+ # When resource is nil it means global notification
+ def update_custom_notification(event, user, resource: nil, value: true)
+ setting = user.notification_settings_for(resource)
+ setting.update!(event => value)
+ end
+end
diff --git a/spec/support/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index 2c501a2a27c..2c501a2a27c 100644
--- a/spec/support/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index 4212be2cc88..4212be2cc88 100644
--- a/spec/support/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
new file mode 100644
index 00000000000..28536bbef5e
--- /dev/null
+++ b/spec/support/helpers/query_recorder.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ class QueryRecorder
+ attr_reader :log, :cached
+
+ def initialize(&block)
+ @log = []
+ @cached = []
+ ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
+ end
+
+ def show_backtrace(values)
+ Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
+ caller.each { |line| Rails.logger.debug(" --> #{line}") }
+ end
+
+ def callback(name, start, finish, message_id, values)
+ show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
+
+ if values[:name]&.include?("CACHE")
+ @cached << values[:sql]
+ elsif !values[:name]&.include?("SCHEMA")
+ @log << values[:sql]
+ end
+ end
+
+ def count
+ @log.count
+ end
+
+ def cached_count
+ @cached.count
+ end
+
+ def log_message
+ @log.join("\n\n")
+ end
+ end
+end
diff --git a/spec/support/helpers/quick_actions_helpers.rb b/spec/support/helpers/quick_actions_helpers.rb
new file mode 100644
index 00000000000..361190aa352
--- /dev/null
+++ b/spec/support/helpers/quick_actions_helpers.rb
@@ -0,0 +1,10 @@
+module QuickActionsHelpers
+ def write_note(text)
+ Sidekiq::Testing.fake! do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: text
+ find('.js-comment-submit-button').click
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb
new file mode 100644
index 00000000000..acd9cce6a67
--- /dev/null
+++ b/spec/support/helpers/rake_helpers.rb
@@ -0,0 +1,23 @@
+module RakeHelpers
+ def run_rake_task(task_name, *args)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task("#{task_name}[#{args.join(',')}]")
+ end
+
+ def stub_warn_user_is_not_gitlab
+ allow(main_object).to receive(:warn_user_is_not_gitlab)
+ end
+
+ def silence_output
+ allow(main_object).to receive(:puts)
+ allow(main_object).to receive(:print)
+ end
+
+ def silence_progress_bar
+ allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double().as_null_object)
+ end
+
+ def main_object
+ @main_object ||= TOPLEVEL_BINDING.eval('self')
+ end
+end
diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb
index e22dd974c6a..e22dd974c6a 100644
--- a/spec/support/reactive_caching_helpers.rb
+++ b/spec/support/helpers/reactive_caching_helpers.rb
diff --git a/spec/support/redis_without_keys.rb b/spec/support/helpers/redis_without_keys.rb
index 6220167dee6..6220167dee6 100644
--- a/spec/support/redis_without_keys.rb
+++ b/spec/support/helpers/redis_without_keys.rb
diff --git a/spec/support/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb
index c01897ed1a1..c01897ed1a1 100644
--- a/spec/support/reference_parser_helpers.rb
+++ b/spec/support/helpers/reference_parser_helpers.rb
diff --git a/spec/support/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index 3c6956cf5e0..3c6956cf5e0 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
diff --git a/spec/support/helpers/routes_helpers.rb b/spec/support/helpers/routes_helpers.rb
new file mode 100644
index 00000000000..c4129606418
--- /dev/null
+++ b/spec/support/helpers/routes_helpers.rb
@@ -0,0 +1,7 @@
+module RoutesHelpers
+ def fake_routes(&block)
+ @routes = @routes.dup
+ @routes.formatter.clear
+ ActionDispatch::Routing::Mapper.new(@routes).instance_exec(&block)
+ end
+end
diff --git a/spec/support/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index abbbb636d66..abbbb636d66 100644
--- a/spec/support/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb
new file mode 100644
index 00000000000..8fd107260cc
--- /dev/null
+++ b/spec/support/helpers/seed_helper.rb
@@ -0,0 +1,110 @@
+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
+
+module SeedHelper
+ GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze
+
+ def ensure_seeds
+ if File.exist?(SEED_STORAGE_PATH)
+ FileUtils.rm_r(SEED_STORAGE_PATH)
+ end
+
+ FileUtils.mkdir_p(SEED_STORAGE_PATH)
+
+ create_bare_seeds
+ create_normal_seeds
+ create_mutable_seeds
+ create_broken_seeds
+ create_git_attributes
+ create_invalid_git_attributes
+ end
+
+ def create_bare_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_normal_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_mutable_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+
+ mutable_repo_full_path = File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH)
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} branch -t feature origin/feature),
+ chdir: mutable_repo_full_path, out: '/dev/null', err: '/dev/null')
+
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}),
+ chdir: mutable_repo_full_path, out: '/dev/null', err: '/dev/null')
+ end
+
+ def create_broken_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
+ chdir: SEED_STORAGE_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+
+ refs_path = File.join(SEED_STORAGE_PATH, TEST_BROKEN_REPO_PATH, 'refs')
+
+ FileUtils.rm_r(refs_path)
+ end
+
+ def create_git_attributes
+ dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ File.open(File.join(dir, 'attributes'), 'w') do |handle|
+ handle.write <<-EOF.strip
+# This is a comment, it should be ignored.
+
+*.txt text
+*.jpg -text
+*.sh eol=lf gitlab-language=shell
+*.haml.* gitlab-language=haml
+foo/bar.* foo
+*.cgi key=value?p1=v1&p2=v2
+/*.png gitlab-language=png
+*.binary binary
+
+# This uses a tab instead of spaces to ensure the parser also supports this.
+*.md\tgitlab-language=markdown
+bla/bla.txt
+ EOF
+ end
+ end
+
+ def create_invalid_git_attributes
+ dir = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ enc = Encoding::UTF_16
+
+ File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle|
+ handle.write('# hello'.encode(enc))
+ end
+ end
+
+ # Prevent developer git configurations from being persisted to test
+ # repositories
+ def git_env
+ { 'GIT_TEMPLATE_DIR' => '' }
+ end
+end
diff --git a/spec/support/seed_repo.rb b/spec/support/helpers/seed_repo.rb
index b4868e82cd7..b4868e82cd7 100644
--- a/spec/support/seed_repo.rb
+++ b/spec/support/helpers/seed_repo.rb
diff --git a/spec/support/select2_helper.rb b/spec/support/helpers/select2_helper.rb
index 90618ba5b19..90618ba5b19 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/helpers/select2_helper.rb
diff --git a/spec/support/selection_helper.rb b/spec/support/helpers/selection_helper.rb
index b4725b137b2..b4725b137b2 100644
--- a/spec/support/selection_helper.rb
+++ b/spec/support/helpers/selection_helper.rb
diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb
new file mode 100644
index 00000000000..9496a94d8f4
--- /dev/null
+++ b/spec/support/helpers/sorting_helper.rb
@@ -0,0 +1,18 @@
+# Helper allows you to sort items
+#
+# Params
+# value - value for sorting
+#
+# Usage:
+# include SortingHelper
+#
+# sorting_by('Oldest updated')
+#
+module SortingHelper
+ def sorting_by(value)
+ find('button.dropdown-toggle').click
+ page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
+ click_link value
+ end
+ end
+end
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
new file mode 100644
index 00000000000..1823099dd9c
--- /dev/null
+++ b/spec/support/helpers/stub_configuration.rb
@@ -0,0 +1,102 @@
+require 'active_support/core_ext/hash/transform_values'
+require 'active_support/hash_with_indifferent_access'
+
+module StubConfiguration
+ def stub_application_setting(messages)
+ add_predicates(messages)
+
+ # Stubbing both of these because we're not yet consistent with how we access
+ # current application settings
+ allow_any_instance_of(ApplicationSetting).to receive_messages(to_settings(messages))
+ allow(Gitlab::CurrentSettings.current_application_settings)
+ .to receive_messages(to_settings(messages))
+
+ # Ensure that we don't use the Markdown cache when stubbing these values
+ allow_any_instance_of(ApplicationSetting).to receive(:cached_html_up_to_date?).and_return(false)
+ end
+
+ def stub_not_protect_default_branch
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ def stub_config_setting(messages)
+ allow(Gitlab.config.gitlab).to receive_messages(to_settings(messages))
+ end
+
+ def stub_gravatar_setting(messages)
+ allow(Gitlab.config.gravatar).to receive_messages(to_settings(messages))
+ end
+
+ def stub_incoming_email_setting(messages)
+ allow(Gitlab.config.incoming_email).to receive_messages(to_settings(messages))
+ end
+
+ def stub_mattermost_setting(messages)
+ allow(Gitlab.config.mattermost).to receive_messages(to_settings(messages))
+ end
+
+ def stub_omniauth_setting(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(to_settings(messages))
+ end
+
+ def stub_backup_setting(messages)
+ allow(Gitlab.config.backup).to receive_messages(to_settings(messages))
+ end
+
+ def stub_lfs_setting(messages)
+ allow(Gitlab.config.lfs).to receive_messages(to_settings(messages))
+ end
+
+ def stub_artifacts_setting(messages)
+ allow(Gitlab.config.artifacts).to receive_messages(to_settings(messages))
+ end
+
+ def stub_storage_settings(messages)
+ messages.deep_stringify_keys!
+
+ # Default storage is always required
+ messages['default'] ||= Gitlab.config.repositories.storages.default
+ messages.each do |storage_name, storage_hash|
+ if !storage_hash.key?('path') || storage_hash['path'] == Gitlab::GitalyClient::StorageSettings::Deprecated
+ storage_hash['path'] = TestEnv.repos_path
+ end
+
+ messages[storage_name] = Gitlab::GitalyClient::StorageSettings.new(storage_hash.to_h)
+ end
+
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
+ end
+
+ private
+
+ # Modifies stubbed messages to also stub possible predicate versions
+ #
+ # Examples:
+ #
+ # add_predicates(foo: true)
+ # # => {foo: true, foo?: true}
+ #
+ # add_predicates(signup_enabled?: false)
+ # # => {signup_enabled? false}
+ def add_predicates(messages)
+ # Only modify keys that aren't already predicates
+ keys = messages.keys.map(&:to_s).reject { |k| k.end_with?('?') }
+
+ keys.each do |key|
+ predicate = key + '?'
+ messages[predicate.to_sym] = messages[key.to_sym]
+ end
+ end
+
+ # Support nested hashes by converting all values into Settingslogic objects
+ def to_settings(hash)
+ hash.transform_values do |value|
+ if value.is_a? Hash
+ Settingslogic.new(value.deep_stringify_keys)
+ else
+ value
+ end
+ end
+ end
+end
diff --git a/spec/support/stub_env.rb b/spec/support/helpers/stub_env.rb
index 36b90fc68d6..36b90fc68d6 100644
--- a/spec/support/stub_env.rb
+++ b/spec/support/helpers/stub_env.rb
diff --git a/spec/support/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index b96338bf548..b96338bf548 100644
--- a/spec/support/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index c1618f5086c..c1618f5086c 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
diff --git a/spec/support/stub_gitlab_data.rb b/spec/support/helpers/stub_gitlab_data.rb
index fa402f35b95..fa402f35b95 100644
--- a/spec/support/stub_gitlab_data.rb
+++ b/spec/support/helpers/stub_gitlab_data.rb
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
new file mode 100644
index 00000000000..19d744b959a
--- /dev/null
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -0,0 +1,48 @@
+module StubObjectStorage
+ def stub_object_storage_uploader(
+ config:,
+ uploader:,
+ remote_directory:,
+ enabled: true,
+ proxy_download: false,
+ background_upload: false,
+ direct_upload: false
+ )
+ allow(config).to receive(:enabled) { enabled }
+ allow(config).to receive(:proxy_download) { proxy_download }
+ allow(config).to receive(:background_upload) { background_upload }
+ allow(config).to receive(:direct_upload) { direct_upload }
+
+ return unless enabled
+
+ Fog.mock!
+
+ ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
+ begin
+ connection.directories.create(key: remote_directory)
+ rescue Excon::Error::Conflict
+ end
+ end
+ end
+
+ def stub_artifacts_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
+ uploader: JobArtifactUploader,
+ remote_directory: 'artifacts',
+ **params)
+ end
+
+ def stub_lfs_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.lfs.object_store,
+ uploader: LfsObjectUploader,
+ remote_directory: 'lfs-objects',
+ **params)
+ end
+
+ def stub_uploads_object_storage(uploader = described_class, **params)
+ stub_object_storage_uploader(config: Gitlab.config.uploads.object_store,
+ uploader: uploader,
+ remote_directory: 'uploads',
+ **params)
+ end
+end
diff --git a/spec/support/helpers/terms_helper.rb b/spec/support/helpers/terms_helper.rb
new file mode 100644
index 00000000000..a00ec14138b
--- /dev/null
+++ b/spec/support/helpers/terms_helper.rb
@@ -0,0 +1,19 @@
+module TermsHelper
+ def enforce_terms
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ settings = Gitlab::CurrentSettings.current_application_settings
+ ApplicationSettings::UpdateService.new(
+ settings, nil, terms: 'These are the terms', enforce_terms: true
+ ).execute
+ end
+
+ def accept_terms(user)
+ terms = Gitlab::CurrentSettings.current_application_settings.latest_terms
+ Users::RespondToTermsService.new(user, terms).execute(accepted: true)
+ end
+
+ def expect_to_be_on_terms_page
+ expect(current_path).to eq terms_path
+ expect(page).to have_content('Please accept the Terms of Service before continuing.')
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
new file mode 100644
index 00000000000..57aa07cf4fa
--- /dev/null
+++ b/spec/support/helpers/test_env.rb
@@ -0,0 +1,371 @@
+require 'rspec/mocks'
+require 'toml-rb'
+
+module TestEnv
+ extend self
+
+ ComponentFailedToInstallError = Class.new(StandardError)
+
+ # When developing the seed repository, comment out the branch you will modify.
+ BRANCH_SHA = {
+ 'signed-commits' => '2d1096e',
+ 'not-merged-branch' => 'b83d6e3',
+ 'branch-merged' => '498214d',
+ 'empty-branch' => '7efb185',
+ 'ends-with.json' => '98b0d8b',
+ 'flatten-dir' => 'e56497b',
+ 'feature' => '0b4bc9a',
+ 'feature_conflict' => 'bb5206f',
+ 'fix' => '48f0be4',
+ 'improve/awesome' => '5937ac0',
+ 'merged-target' => '21751bf',
+ 'markdown' => '0ed8c6c',
+ 'lfs' => '55bc176',
+ 'master' => 'b83d6e3',
+ 'merge-test' => '5937ac0',
+ "'test'" => 'e56497b',
+ 'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
+ 'gitattributes' => '5a62481',
+ 'expand-collapse-diffs' => '4842455',
+ 'symlink-expand-diff' => '81e6355',
+ 'expand-collapse-files' => '025db92',
+ 'expand-collapse-lines' => '238e82d',
+ 'video' => '8879059',
+ 'add-balsamiq-file' => 'b89b56d',
+ 'crlf-diff' => '5938907',
+ 'conflict-start' => '824be60',
+ 'conflict-resolvable' => '1450cd6',
+ 'conflict-binary-file' => '259a6fb',
+ 'conflict-contains-conflict-markers' => '78a3086',
+ 'conflict-missing-side' => 'eb227b3',
+ 'conflict-non-utf8' => 'd0a293c',
+ 'conflict-too-large' => '39fa04f',
+ 'deleted-image-test' => '6c17798',
+ 'wip' => 'b9238ee',
+ 'csv' => '3dd0896',
+ 'v1.1.0' => 'b83d6e3',
+ 'add-ipython-files' => '93ee732',
+ 'add-pdf-file' => 'e774ebd',
+ 'add-pdf-text-binary' => '79faa7b',
+ 'add_images_and_changes' => '010d106'
+ }.freeze
+
+ # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
+ # need to keep all the branches in sync.
+ # We currently only need a subset of the branches
+ FORKED_BRANCH_SHA = {
+ 'add-submodule-version-bump' => '3f547c0',
+ 'master' => '5937ac0',
+ 'remove-submodule' => '2a33e0c',
+ 'conflict-resolvable-fork' => '404fa3f'
+ }.freeze
+
+ TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
+ REPOS_STORAGE = 'default'.freeze
+
+ # Test environment
+ #
+ # See gitlab.yml.example test section for paths
+ #
+ def init(opts = {})
+ unless Rails.env.test?
+ puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
+ exit 1
+ end
+
+ # Disable mailer for spinach tests
+ disable_mailer if opts[:mailer] == false
+
+ clean_test_path
+
+ # Setup GitLab shell for test instance
+ setup_gitlab_shell
+
+ setup_gitaly
+
+ # Create repository for FactoryBot.create(:project)
+ setup_factory_repo
+
+ # Create repository for FactoryBot.create(:forked_project_with_submodules)
+ setup_forked_repo
+ end
+
+ def disable_mailer
+ allow_any_instance_of(NotificationService).to receive(:mailer)
+ .and_return(double.as_null_object)
+ end
+
+ def enable_mailer
+ allow_any_instance_of(NotificationService).to receive(:mailer)
+ .and_call_original
+ end
+
+ def disable_pre_receive
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+ end
+
+ # Clean /tmp/tests
+ #
+ # Keeps gitlab-shell and gitlab-test
+ def clean_test_path
+ Dir[TMP_TEST_PATH].each do |entry|
+ unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/
+ FileUtils.rm_rf(entry)
+ end
+ end
+
+ FileUtils.mkdir_p(repos_path)
+ FileUtils.mkdir_p(backup_path)
+ FileUtils.mkdir_p(pages_path)
+ FileUtils.mkdir_p(artifacts_path)
+ end
+
+ def clean_gitlab_test_path
+ Dir[TMP_TEST_PATH].each do |entry|
+ if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/
+ FileUtils.rm_rf(entry)
+ end
+ end
+ end
+
+ def setup_gitlab_shell
+ component_timed_setup('GitLab Shell',
+ install_dir: Gitlab.config.gitlab_shell.path,
+ version: Gitlab::Shell.version_required,
+ task: 'gitlab:shell:install')
+ end
+
+ def setup_gitaly
+ socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
+ gitaly_dir = File.dirname(socket_path)
+
+ component_timed_setup('Gitaly',
+ install_dir: gitaly_dir,
+ version: Gitlab::GitalyClient.expected_server_version,
+ task: "gitlab:gitaly:install[#{gitaly_dir}]") do
+
+ # Always re-create config, in case it's outdated. This is fast anyway.
+ Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true)
+
+ start_gitaly(gitaly_dir)
+ end
+ end
+
+ def start_gitaly(gitaly_dir)
+ if ENV['CI'].present?
+ # Gitaly has been spawned outside this process already
+ return
+ end
+
+ spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
+ Bundler.with_original_env do
+ raise "gitaly spawn failed" unless system(spawn_script)
+ end
+ @gitaly_pid = Integer(File.read('tmp/tests/gitaly.pid'))
+
+ Kernel.at_exit { stop_gitaly }
+
+ wait_gitaly
+ end
+
+ def wait_gitaly
+ sleep_time = 10
+ sleep_interval = 0.1
+ socket = Gitlab::GitalyClient.address('default').sub('unix:', '')
+
+ Integer(sleep_time / sleep_interval).times do
+ begin
+ Socket.unix(socket)
+ return
+ rescue
+ sleep sleep_interval
+ end
+ end
+
+ raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
+ end
+
+ def stop_gitaly
+ return unless @gitaly_pid
+
+ Process.kill('KILL', @gitaly_pid)
+ rescue Errno::ESRCH
+ # The process can already be gone if the test run was INTerrupted.
+ end
+
+ def setup_factory_repo
+ setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
+ BRANCH_SHA)
+ end
+
+ # This repo has a submodule commit that is not present in the main test
+ # repository.
+ def setup_forked_repo
+ setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name,
+ FORKED_BRANCH_SHA)
+ end
+
+ def setup_repo(repo_path, repo_path_bare, repo_name, refs)
+ clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
+
+ unless File.directory?(repo_path)
+ system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
+ end
+
+ set_repo_refs(repo_path, refs)
+
+ unless File.directory?(repo_path_bare)
+ # We must copy bare repositories because we will push to them.
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
+ end
+ end
+
+ def copy_repo(project, bare_repo:, refs:)
+ target_repo_path = File.expand_path(repos_path + "/#{project.disk_path}.git")
+
+ FileUtils.mkdir_p(target_repo_path)
+ FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path)
+ FileUtils.chmod_R 0755, target_repo_path
+ set_repo_refs(target_repo_path, refs)
+ end
+
+ def repos_path
+ @repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
+ end
+
+ def backup_path
+ Gitlab.config.backup.path
+ end
+
+ def pages_path
+ Gitlab.config.pages.path
+ end
+
+ def artifacts_path
+ Gitlab.config.artifacts.storage_path
+ end
+
+ # When no cached assets exist, manually hit the root path to create them
+ #
+ # Otherwise they'd be created by the first test, often timing out and
+ # causing a transient test failure
+ def eager_load_driver_server
+ return unless defined?(Capybara)
+
+ puts "Starting the Capybara driver server..."
+ Capybara.current_session.visit '/'
+ end
+
+ def factory_repo_path_bare
+ "#{factory_repo_path}_bare"
+ end
+
+ def forked_repo_path_bare
+ "#{forked_repo_path}_bare"
+ end
+
+ def with_empty_bare_repository(name = nil)
+ path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s
+
+ yield(Rugged::Repository.init_at(path, :bare))
+ ensure
+ FileUtils.rm_rf(path)
+ end
+
+ private
+
+ def factory_repo_path
+ @factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name)
+ end
+
+ def factory_repo_name
+ 'gitlab-test'
+ end
+
+ def forked_repo_path
+ @forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name)
+ end
+
+ def forked_repo_name
+ 'gitlab-test-fork'
+ end
+
+ # Prevent developer git configurations from being persisted to test
+ # repositories
+ def git_env
+ { 'GIT_TEMPLATE_DIR' => '' }
+ end
+
+ def set_repo_refs(repo_path, branch_sha)
+ instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
+ update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
+ reset = proc do
+ Dir.chdir(repo_path) do
+ IO.popen(update_refs, "w") { |io| io.write(instructions) }
+ $?.success?
+ end
+ end
+
+ # Try to reset without fetching to avoid using the network.
+ unless reset.call
+ raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
+
+ # Before we used Git clone's --mirror option, bare repos could end up
+ # with missing refs, clearing them and retrying should fix the issue.
+ clean_gitlab_test_path && init unless reset.call
+ end
+ end
+
+ def component_timed_setup(component, install_dir:, version:, task:)
+ puts "\n==> Setting up #{component}..."
+ start = Time.now
+
+ ensure_component_dir_name_is_correct!(component, install_dir)
+
+ # On CI, once installed, components never need update
+ return if File.exist?(install_dir) && ENV['CI']
+
+ if component_needs_update?(install_dir, version)
+ # Cleanup the component entirely to ensure we start fresh
+ FileUtils.rm_rf(install_dir)
+
+ unless system('rake', task)
+ raise ComponentFailedToInstallError
+ end
+ end
+
+ yield if block_given?
+
+ rescue ComponentFailedToInstallError
+ puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"
+ FileUtils.rm_rf(install_dir)
+ exit 1
+ ensure
+ puts " #{component} setup in #{Time.now - start} seconds...\n"
+ end
+
+ def ensure_component_dir_name_is_correct!(component, path)
+ actual_component_dir_name = File.basename(path)
+ expected_component_dir_name = component.parameterize
+
+ unless actual_component_dir_name == expected_component_dir_name
+ puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n"
+ exit 1
+ end
+ end
+
+ def component_needs_update?(component_folder, expected_version)
+ # Allow local overrides of the component for tests during development
+ return false if Rails.env.test? && File.symlink?(component_folder)
+
+ version = File.read(File.join(component_folder, 'VERSION')).strip
+
+ # Notice that this will always yield true when using branch versions
+ # (`=branch_name`), but that actually makes sure the server is always based
+ # on the latest branch revision.
+ version != expected_version
+ rescue Errno::ENOENT
+ true
+ end
+end
diff --git a/spec/support/upload_helpers.rb b/spec/support/helpers/upload_helpers.rb
index 5eead80c935..5eead80c935 100644
--- a/spec/support/upload_helpers.rb
+++ b/spec/support/helpers/upload_helpers.rb
diff --git a/spec/support/user_activities_helpers.rb b/spec/support/helpers/user_activities_helpers.rb
index 44feb104644..44feb104644 100644
--- a/spec/support/user_activities_helpers.rb
+++ b/spec/support/helpers/user_activities_helpers.rb
diff --git a/spec/support/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index fda0e29f983..fda0e29f983 100644
--- a/spec/support/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index ef1f9f68671..ef1f9f68671 100644
--- a/spec/support/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
deleted file mode 100644
index e61983c60b4..00000000000
--- a/spec/support/issuables_list_metadata_shared_examples.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
- before do
- @issuable_ids = []
-
- %w[fix improve/awesome].each do |source_branch|
- issuable =
- if issuable_type == :issue
- create(issuable_type, project: project, author: project.creator)
- else
- create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator)
- end
-
- @issuable_ids << issuable.id
- end
- end
-
- it "creates indexed meta-data object for issuable notes and votes count" do
- if action
- get action, author_id: project.creator.id
- else
- get :index, namespace_id: project.namespace, project_id: project
- end
-
- meta_data = assigns(:issuable_meta_data)
-
- aggregate_failures do
- expect(meta_data.keys).to match_array(@issuable_ids)
- expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta))
- end
- end
-
- describe "when given empty collection" do
- let(:project2) { create(:project, :public) }
-
- it "doesn't execute any queries with false conditions" do
- get_action =
- if action
- proc { get action, author_id: project.creator.id }
- else
- proc { get :index, namespace_id: project2.namespace, project_id: project2 }
- end
-
- expect(&get_action).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
- end
- end
-end
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
deleted file mode 100644
index 2197bc9d853..00000000000
--- a/spec/support/javascript_fixtures_helpers.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-require 'action_dispatch/testing/test_request'
-require 'fileutils'
-
-module JavaScriptFixturesHelpers
- include Gitlab::Popen
-
- FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
-
- # Public: Removes all fixture files from given directory
- #
- # directory_name - directory of the fixtures (relative to FIXTURE_PATH)
- #
- def clean_frontend_fixtures(directory_name)
- directory_name = File.expand_path(directory_name, FIXTURE_PATH)
- Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name|
- FileUtils.rm(file_name)
- end
- end
-
- # Public: Store a response object as fixture file
- #
- # response - string or response object to store
- # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
- #
- def store_frontend_fixture(response, fixture_file_name)
- fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
- fixture = response.respond_to?(:body) ? parse_response(response) : response
-
- FileUtils.mkdir_p(File.dirname(fixture_file_name))
- File.write(fixture_file_name, fixture)
- end
-
- def remove_repository(project)
- Gitlab::Shell.new.remove_repository(project.repository_storage_path, project.disk_path)
- end
-
- private
-
- # Private: Prepare a response object for use as a frontend fixture
- #
- # response - response object to prepare
- #
- def parse_response(response)
- fixture = response.body
- fixture.force_encoding("utf-8")
-
- response_mime_type = Mime::Type.lookup(response.content_type)
- if response_mime_type.html?
- doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
-
- link_tags = doc.css('link')
- link_tags.remove
-
- scripts = doc.css("script:not([type='text/template']):not([type='text/x-template'])")
- scripts.remove
-
- fixture = doc.to_html
-
- # replace relative links
- test_host = ActionDispatch::TestRequest::DEFAULT_ENV['HTTP_HOST']
- fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
- end
-
- fixture
- end
-end
diff --git a/spec/support/json_response.rb b/spec/support/json_response.rb
new file mode 100644
index 00000000000..210b0e6d867
--- /dev/null
+++ b/spec/support/json_response.rb
@@ -0,0 +1,5 @@
+RSpec.configure do |config|
+ config.include_context 'JSON response'
+ config.include_context 'JSON response', type: :request
+ config.include_context 'JSON response', :api
+end
diff --git a/spec/support/json_response_helpers.rb b/spec/support/json_response_helpers.rb
deleted file mode 100644
index aa235529c56..00000000000
--- a/spec/support/json_response_helpers.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-shared_context 'JSON response' do
- let(:json_response) { JSON.parse(response.body) }
-end
-
-RSpec.configure do |config|
- config.include_context 'JSON response'
- config.include_context 'JSON response', type: :request
- config.include_context 'JSON response', :api
-end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
deleted file mode 100644
index e46b61b6461..00000000000
--- a/spec/support/kubernetes_helpers.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-module KubernetesHelpers
- include Gitlab::Kubernetes
-
- def kube_response(body)
- { body: body.to_json }
- end
-
- def kube_pods_response
- kube_response(kube_pods_body)
- end
-
- def stub_kubeclient_discover(api_url)
- WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
- end
-
- def stub_kubeclient_pods(response = nil)
- stub_kubeclient_discover(service.api_url)
- pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
-
- WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
- end
-
- def stub_kubeclient_get_secrets(api_url, **options)
- WebMock.stub_request(:get, api_url + '/api/v1/secrets')
- .to_return(kube_response(kube_v1_secrets_body(options)))
- end
-
- def stub_kubeclient_get_secrets_error(api_url)
- WebMock.stub_request(:get, api_url + '/api/v1/secrets')
- .to_return(status: [404, "Internal Server Error"])
- end
-
- def kube_v1_secrets_body(**options)
- {
- "kind" => "SecretList",
- "apiVersion": "v1",
- "items" => [
- {
- "metadata": {
- "name": options[:metadata_name] || "default-token-1",
- "namespace": "kube-system"
- },
- "data": {
- "token": options[:token] || Base64.encode64('token-sample-123')
- }
- }
- ]
- }
- end
-
- def kube_v1_discovery_body
- {
- "kind" => "APIResourceList",
- "resources" => [
- { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
- { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }
- ]
- }
- end
-
- def kube_pods_body
- {
- "kind" => "PodList",
- "items" => [kube_pod]
- }
- end
-
- # This is a partial response, it will have many more elements in reality but
- # these are the ones we care about at the moment
- def kube_pod(name: "kube-pod", app: "valid-pod-label")
- {
- "metadata" => {
- "name" => name,
- "creationTimestamp" => "2016-11-25T19:55:19Z",
- "labels" => { "app" => app }
- },
- "spec" => {
- "containers" => [
- { "name" => "container-0" },
- { "name" => "container-1" }
- ]
- },
- "status" => { "phase" => "Running" }
- }
- end
-
- def kube_terminals(service, pod)
- pod_name = pod['metadata']['name']
- containers = pod['spec']['containers']
-
- containers.map do |container|
- terminal = {
- selectors: { pod: pod_name, container: container['name'] },
- url: container_exec_url(service.api_url, service.actual_namespace, pod_name, container['name']),
- subprotocols: ['channel.k8s.io'],
- headers: { 'Authorization' => ["Bearer #{service.token}"] },
- created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
- max_session_time: 0
- }
- terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
- terminal
- end
- end
-end
diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb
deleted file mode 100644
index 0e87b3d359d..00000000000
--- a/spec/support/ldap_helpers.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-module LdapHelpers
- def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap))
- ::Gitlab::Auth::LDAP::Adapter.new(provider, ldap)
- end
-
- def user_dn(uid)
- "uid=#{uid},ou=users,dc=example,dc=com"
- end
-
- # Accepts a hash of Gitlab::Auth::LDAP::Config keys and values.
- #
- # Example:
- # stub_ldap_config(
- # group_base: 'ou=groups,dc=example,dc=com',
- # admin_group: 'my-admin-group'
- # )
- def stub_ldap_config(messages)
- allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages)
- end
-
- # Stub an LDAP person search and provide the return entry. Specify `nil` for
- # `entry` to simulate when an LDAP person is not found
- #
- # Example:
- # adapter = ::Gitlab::Auth::LDAP::Adapter.new('ldapmain', double(:ldap))
- # ldap_user_entry = ldap_user_entry('john_doe')
- #
- # stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter)
- def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain')
- return_value = ::Gitlab::Auth::LDAP::Person.new(entry, provider) if entry.present?
-
- allow(::Gitlab::Auth::LDAP::Person)
- .to receive(:find_by_uid).with(uid, any_args).and_return(return_value)
- end
-
- # Create a simple LDAP user entry.
- def ldap_user_entry(uid)
- entry = Net::LDAP::Entry.new
- entry['dn'] = user_dn(uid)
- entry['uid'] = uid
-
- entry
- end
-
- def raise_ldap_connection_error
- allow_any_instance_of(Gitlab::Auth::LDAP::Adapter)
- .to receive(:ldap_search).and_raise(Gitlab::Auth::LDAP::LDAPConnectionError)
- end
-end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
deleted file mode 100644
index db34090e971..00000000000
--- a/spec/support/login_helpers.rb
+++ /dev/null
@@ -1,162 +0,0 @@
-require_relative 'devise_helpers'
-
-module LoginHelpers
- include DeviseHelpers
-
- # Overriding Devise::Test::IntegrationHelpers#sign_in to store @current_user
- # since we may need it in LiveDebugger#live_debug.
- def sign_in(resource, scope: nil)
- super
-
- @current_user = resource
- end
-
- # Overriding Devise::Test::IntegrationHelpers#sign_out to clear @current_user.
- def sign_out(resource_or_scope)
- super
-
- @current_user = nil
- end
-
- # Internal: Log in as a specific user or a new user of a specific role
- #
- # user_or_role - User object, or a role to create (e.g., :admin, :user)
- #
- # Examples:
- #
- # # Create a user automatically
- # gitlab_sign_in(:user)
- #
- # # Create an admin automatically
- # gitlab_sign_in(:admin)
- #
- # # Provide an existing User record
- # user = create(:user)
- # gitlab_sign_in(user)
- def gitlab_sign_in(user_or_role, **kwargs)
- user =
- if user_or_role.is_a?(User)
- user_or_role
- else
- create(user_or_role)
- end
-
- gitlab_sign_in_with(user, **kwargs)
-
- @current_user = user
- end
-
- def gitlab_sign_in_via(provider, user, uid)
- mock_auth_hash(provider, uid, user.email)
- visit new_user_session_path
- click_link provider
- end
-
- # Requires Javascript driver.
- def gitlab_sign_out
- find(".header-user-dropdown-toggle").click
- click_link "Sign out"
- @current_user = nil
-
- expect(page).to have_button('Sign in')
- end
-
- private
-
- # Private: Login as the specified user
- #
- # user - User instance to login with
- # remember - Whether or not to check "Remember me" (default: false)
- def gitlab_sign_in_with(user, remember: false)
- visit new_user_session_path
-
- fill_in "user_login", with: user.email
- fill_in "user_password", with: "12345678"
- check 'user_remember_me' if remember
-
- click_button "Sign in"
- end
-
- def login_via(provider, user, uid, remember_me: false)
- mock_auth_hash(provider, uid, user.email)
- visit new_user_session_path
- expect(page).to have_content('Sign in with')
-
- check 'remember_me' if remember_me
-
- click_link "oauth-login-#{provider}"
- end
-
- def mock_auth_hash(provider, uid, email)
- # The mock_auth configuration allows you to set per-provider (or default)
- # authentication hashes to return during integration testing.
- OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
- provider: provider,
- uid: uid,
- info: {
- name: 'mockuser',
- email: email,
- image: 'mock_user_thumbnail_url'
- },
- credentials: {
- token: 'mock_token',
- secret: 'mock_secret'
- },
- extra: {
- raw_info: {
- info: {
- name: 'mockuser',
- email: email,
- image: 'mock_user_thumbnail_url'
- }
- }
- }
- })
- Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml]
- end
-
- def mock_saml_config
- OpenStruct.new(name: 'saml', label: 'saml', args: {
- assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
- idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52',
- idp_sso_target_url: 'https://idp.example.com/sso/saml',
- issuer: 'https://localhost:3443/',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
- })
- end
-
- def stub_omniauth_provider(provider, context: Rails.application)
- env = env_from_context(context)
-
- set_devise_mapping(context: context)
- env['omniauth.auth'] = OmniAuth.config.mock_auth[provider]
- end
-
- def stub_omniauth_saml_config(messages)
- set_devise_mapping(context: Rails.application)
- Rails.application.routes.disable_clear_and_finalize = true
- Rails.application.routes.draw do
- post '/users/auth/saml' => 'omniauth_callbacks#saml'
- end
- allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
- stub_omniauth_setting(messages)
- stub_saml_authorize_path_helpers
- end
-
- def stub_saml_authorize_path_helpers
- allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml')
- allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
- end
-
- def stub_omniauth_config(messages)
- allow(Gitlab.config.omniauth).to receive_messages(messages)
- end
-
- def stub_basic_saml_config
- allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
- end
-
- def stub_saml_group_config(groups)
- allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
- end
-end
diff --git a/spec/support/malicious_regexp_shared_examples.rb b/spec/support/malicious_regexp_shared_examples.rb
deleted file mode 100644
index ac5d22298bb..00000000000
--- a/spec/support/malicious_regexp_shared_examples.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-shared_examples 'malicious regexp' do
- let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' }
- let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }
-
- it 'takes under a second' do
- expect { Timeout.timeout(1) { subject } }.not_to raise_error
- end
-end
diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index f4127efc6ae..f4127efc6ae 100644
--- a/spec/support/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
new file mode 100644
index 00000000000..88d22a3ddd9
--- /dev/null
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -0,0 +1,64 @@
+RSpec::Matchers.define :exceed_query_limit do |expected|
+ supports_block_expectations
+
+ match do |block|
+ @subject_block = block
+ actual_count > expected_count + threshold
+ end
+
+ failure_message_when_negated do |actual|
+ threshold_message = threshold > 0 ? " (+#{@threshold})" : ''
+ counts = "#{expected_count}#{threshold_message}"
+ "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
+ end
+
+ def with_threshold(threshold)
+ @threshold = threshold
+ self
+ end
+
+ def for_query(query)
+ @query = query
+ self
+ end
+
+ def threshold
+ @threshold.to_i
+ end
+
+ def expected_count
+ if expected.is_a?(ActiveRecord::QueryRecorder)
+ expected.count
+ else
+ expected
+ end
+ end
+
+ def actual_count
+ @actual_count ||= if @query
+ recorder.log.select { |recorded| recorded =~ @query }.size
+ else
+ recorder.count
+ end
+ end
+
+ def recorder
+ @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
+ end
+
+ def count_queries(queries)
+ queries.each_with_object(Hash.new(0)) { |query, counts| counts[query] += 1 }
+ end
+
+ def log_message
+ if expected.is_a?(ActiveRecord::QueryRecorder)
+ counts = count_queries(expected.log)
+ extra_queries = @recorder.log.reject { |query| counts[query] -= 1 unless counts[query].zero? }
+ extra_queries_display = count_queries(extra_queries).map { |query, count| "[#{count}] #{query}" }
+
+ (['Extra queries:'] + extra_queries_display).join("\n\n")
+ else
+ @recorder.log_message
+ end
+ end
+end
diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb
deleted file mode 100644
index 5d6f662e8fe..00000000000
--- a/spec/support/migrations_helpers.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-module MigrationsHelpers
- def table(name)
- Class.new(ActiveRecord::Base) do
- self.table_name = name
- self.inheritance_column = :_type_disabled
- end
- end
-
- def migrations_paths
- ActiveRecord::Migrator.migrations_paths
- end
-
- def table_exists?(name)
- ActiveRecord::Base.connection.table_exists?(name)
- end
-
- def migrations
- ActiveRecord::Migrator.migrations(migrations_paths)
- end
-
- def clear_schema_cache!
- ActiveRecord::Base.connection_pool.connections.each do |conn|
- conn.schema_cache.clear!
- end
- end
-
- def reset_column_in_all_models
- clear_schema_cache!
-
- # 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))
-
- # Without that, we get errors because of missing attributes, e.g.
- # super: no superclass method `elasticsearch_indexing' for #<ApplicationSetting:0x00007f85628508d8>
- ApplicationSetting.define_attribute_methods
- end
-
- def reset_column_information(klass)
- klass.reset_column_information
- end
-
- def previous_migration
- migrations.each_cons(2) do |previous, migration|
- break previous if migration.name == described_class.name
- end
- end
-
- def migration_schema_version
- metadata_schema = self.class.metadata[:schema]
-
- if metadata_schema == :latest
- migrations.last.version
- else
- metadata_schema || previous_migration.version
- end
- end
-
- def schema_migrate_down!
- disable_migrations_output do
- ActiveRecord::Migrator.migrate(migrations_paths,
- migration_schema_version)
- end
-
- reset_column_in_all_models
- end
-
- def schema_migrate_up!
- reset_column_in_all_models
-
- disable_migrations_output do
- ActiveRecord::Migrator.migrate(migrations_paths)
- end
-
- reset_column_in_all_models
- end
-
- def disable_migrations_output
- ActiveRecord::Migration.verbose = false
-
- yield
- ensure
- ActiveRecord::Migration.verbose = true
- end
-
- def migrate!
- ActiveRecord::Migrator.up(migrations_paths) do |migration|
- migration.name == described_class.name
- end
- end
-end
diff --git a/spec/support/mobile_helpers.rb b/spec/support/mobile_helpers.rb
deleted file mode 100644
index 3b9eb84e824..00000000000
--- a/spec/support/mobile_helpers.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module MobileHelpers
- def resize_screen_xs
- resize_window(767, 768)
- end
-
- def resize_screen_sm
- resize_window(900, 768)
- end
-
- def restore_window_size
- resize_window(1366, 768)
- end
-
- def resize_window(width, height)
- Capybara.current_session.current_window.resize_to(width, height)
- end
-end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
deleted file mode 100644
index e2c23607406..00000000000
--- a/spec/support/notify_shared_examples.rb
+++ /dev/null
@@ -1,199 +0,0 @@
-shared_context 'gitlab email notification' do
- set(:project) { create(:project, :repository) }
- set(:recipient) { create(:user, email: 'recipient@example.com') }
-
- let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
- let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
- let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
- let(:new_user_address) { 'newguy@example.com' }
-
- before do
- email = recipient.emails.create(email: "notifications@example.com")
- recipient.update_attribute(:notification_email, email.email)
- stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
- end
-end
-
-shared_context 'reply-by-email is enabled with incoming address without %{key}' do
- before do
- stub_incoming_email_setting(enabled: true, address: "reply@#{Gitlab.config.gitlab.host}")
- end
-end
-
-shared_examples 'a multiple recipients email' do
- it 'is sent to the given recipient' do
- is_expected.to deliver_to recipient.notification_email
- end
-end
-
-shared_examples 'an email sent from GitLab' do
- it 'has the characteristics of an email sent from GitLab' do
- sender = subject.header[:from].addrs[0]
- reply_to = subject.header[:reply_to].addresses
-
- aggregate_failures do
- expect(sender.display_name).to eq(gitlab_sender_display_name)
- expect(sender.address).to eq(gitlab_sender)
- expect(reply_to).to eq([gitlab_sender_reply_to])
- end
- end
-end
-
-shared_examples 'an email that contains a header with author username' do
- it 'has X-GitLab-Author header containing author\'s username' do
- is_expected.to have_header 'X-GitLab-Author', user.username
- end
-end
-
-shared_examples 'an email with X-GitLab headers containing project details' do
- it 'has X-GitLab-Project headers' do
- aggregate_failures do
- is_expected.to have_header('X-GitLab-Project', /#{project.name}/)
- is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/)
- is_expected.to have_header('X-GitLab-Project-Path', /#{project.full_path}/)
- end
- end
-end
-
-shared_examples 'a new thread email with reply-by-email enabled' do
- it 'has the characteristics of a threaded email' do
- host = Gitlab.config.gitlab.host
- route_key = "#{model.class.model_name.singular_route_key}_#{model.id}"
-
- aggregate_failures do
- is_expected.to have_header('Message-ID', "<#{route_key}@#{host}>")
- is_expected.to have_header('References', /\A<reply\-.*@#{host}>\Z/ )
- end
- end
-end
-
-shared_examples 'a thread answer email with reply-by-email enabled' do
- include_examples 'an email with X-GitLab headers containing project details'
-
- it 'has the characteristics of a threaded reply' do
- host = Gitlab.config.gitlab.host
- route_key = "#{model.class.model_name.singular_route_key}_#{model.id}"
-
- aggregate_failures do
- is_expected.to have_header('Message-ID', /\A<.*@#{host}>\Z/)
- is_expected.to have_header('In-Reply-To', "<#{route_key}@#{host}>")
- is_expected.to have_header('References', /\A<#{route_key}@#{host}> <reply\-.*@#{host}>\Z/ )
- is_expected.to have_subject(/^Re: /)
- end
- end
-end
-
-shared_examples 'an email starting a new thread with reply-by-email enabled' do
- include_examples 'an email with X-GitLab headers containing project details'
- include_examples 'a new thread email with reply-by-email enabled'
-
- context 'when reply-by-email is enabled with incoming address with %{key}' do
- it 'has a Reply-To header' do
- is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
- end
- end
-
- context 'when reply-by-email is enabled with incoming address without %{key}' do
- include_context 'reply-by-email is enabled with incoming address without %{key}'
- include_examples 'a new thread email with reply-by-email enabled'
-
- it 'has a Reply-To header' do
- is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
- end
- end
-end
-
-shared_examples 'an answer to an existing thread with reply-by-email enabled' do
- include_examples 'an email with X-GitLab headers containing project details'
- include_examples 'a thread answer email with reply-by-email enabled'
-
- context 'when reply-by-email is enabled with incoming address with %{key}' do
- it 'has a Reply-To header' do
- is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
- end
- end
-
- context 'when reply-by-email is enabled with incoming address without %{key}' do
- include_context 'reply-by-email is enabled with incoming address without %{key}'
- include_examples 'a thread answer email with reply-by-email enabled'
-
- it 'has a Reply-To header' do
- is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
- end
- end
-end
-
-shared_examples 'it should have Gmail Actions links' do
- it do
- aggregate_failures do
- is_expected.to have_body_text('<script type="application/ld+json">')
- is_expected.to have_body_text('ViewAction')
- end
- end
-end
-
-shared_examples 'it should not have Gmail Actions links' do
- it do
- aggregate_failures do
- is_expected.not_to have_body_text('<script type="application/ld+json">')
- is_expected.not_to have_body_text('ViewAction')
- end
- end
-end
-
-shared_examples 'it should show Gmail Actions View Issue link' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it { is_expected.to have_body_text('View Issue') }
-end
-
-shared_examples 'it should show Gmail Actions View Merge request link' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it { is_expected.to have_body_text('View Merge request') }
-end
-
-shared_examples 'it should show Gmail Actions View Commit link' do
- it_behaves_like 'it should have Gmail Actions links'
-
- it { is_expected.to have_body_text('View Commit') }
-end
-
-shared_examples 'an unsubscribeable thread' do
- it_behaves_like 'an unsubscribeable thread with incoming address without %{key}'
-
- it 'has a List-Unsubscribe header in the correct format, and a body link' do
- aggregate_failures do
- is_expected.to have_header('List-Unsubscribe', /unsubscribe/)
- is_expected.to have_header('List-Unsubscribe', /mailto/)
- is_expected.to have_header('List-Unsubscribe', /^<.+,.+>$/)
- is_expected.to have_body_text('unsubscribe')
- end
- end
-end
-
-shared_examples 'an unsubscribeable thread with incoming address without %{key}' do
- include_context 'reply-by-email is enabled with incoming address without %{key}'
-
- it 'has a List-Unsubscribe header in the correct format, and a body link' do
- aggregate_failures do
- is_expected.to have_header('List-Unsubscribe', /unsubscribe/)
- is_expected.not_to have_header('List-Unsubscribe', /mailto/)
- is_expected.to have_header('List-Unsubscribe', /^<[^,]+>$/)
- is_expected.to have_body_text('unsubscribe')
- end
- end
-end
-
-shared_examples 'a user cannot unsubscribe through footer link' do
- it 'does not have a List-Unsubscribe header or a body link' do
- aggregate_failures do
- is_expected.not_to have_header('List-Unsubscribe', /unsubscribe/)
- is_expected.not_to have_body_text('unsubscribe')
- end
- end
-end
-
-shared_examples 'an email with a labels subscriptions link in its footer' do
- it { is_expected.to have_body_text('label subscriptions') }
-end
diff --git a/spec/support/prepare-gitlab-git-test-for-commit b/spec/support/prepare-gitlab-git-test-for-commit
index 3047786a599..d08e3ba5481 100755
--- a/spec/support/prepare-gitlab-git-test-for-commit
+++ b/spec/support/prepare-gitlab-git-test-for-commit
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
abort unless [
- system('spec/support/generate-seed-repo-rb', out: 'spec/support/seed_repo.rb'),
+ system('spec/support/generate-seed-repo-rb', out: 'spec/support/helpers/seed_repo.rb'),
system('spec/support/unpack-gitlab-git-test')
].all?
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
deleted file mode 100644
index 8cf8f45a8b2..00000000000
--- a/spec/support/query_recorder.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-module ActiveRecord
- class QueryRecorder
- attr_reader :log, :cached
-
- def initialize(&block)
- @log = []
- @cached = []
- ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
- end
-
- def show_backtrace(values)
- Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
- caller.each { |line| Rails.logger.debug(" --> #{line}") }
- end
-
- def callback(name, start, finish, message_id, values)
- show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
-
- if values[:name]&.include?("CACHE")
- @cached << values[:sql]
- elsif !values[:name]&.include?("SCHEMA")
- @log << values[:sql]
- end
- end
-
- def count
- @log.count
- end
-
- def cached_count
- @cached.count
- end
-
- def log_message
- @log.join("\n\n")
- end
- end
-end
-
-RSpec::Matchers.define :exceed_query_limit do |expected|
- supports_block_expectations
-
- match do |block|
- @subject_block = block
- actual_count > expected_count + threshold
- end
-
- failure_message_when_negated do |actual|
- threshold_message = threshold > 0 ? " (+#{@threshold})" : ''
- counts = "#{expected_count}#{threshold_message}"
- "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
- end
-
- def with_threshold(threshold)
- @threshold = threshold
- self
- end
-
- def for_query(query)
- @query = query
- self
- end
-
- def threshold
- @threshold.to_i
- end
-
- def expected_count
- if expected.is_a?(ActiveRecord::QueryRecorder)
- expected.count
- else
- expected
- end
- end
-
- def actual_count
- @actual_count ||= if @query
- recorder.log.select { |recorded| recorded =~ @query }.size
- else
- recorder.count
- end
- end
-
- def recorder
- @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
- end
-
- def count_queries(queries)
- queries.each_with_object(Hash.new(0)) { |query, counts| counts[query] += 1 }
- end
-
- def log_message
- if expected.is_a?(ActiveRecord::QueryRecorder)
- counts = count_queries(expected.log)
- extra_queries = @recorder.log.reject { |query| counts[query] -= 1 unless counts[query].zero? }
- extra_queries_display = count_queries(extra_queries).map { |query, count| "[#{count}] #{query}" }
-
- (['Extra queries:'] + extra_queries_display).join("\n\n")
- else
- @recorder.log_message
- end
- end
-end
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
deleted file mode 100644
index 86bfeed107c..00000000000
--- a/spec/support/rake_helpers.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module RakeHelpers
- def run_rake_task(task_name, *args)
- Rake::Task[task_name].reenable
- Rake.application.invoke_task("#{task_name}[#{args.join(',')}]")
- end
-
- def stub_warn_user_is_not_gitlab
- allow(main_object).to receive(:warn_user_is_not_gitlab)
- end
-
- def silence_output
- allow(main_object).to receive(:puts)
- allow(main_object).to receive(:print)
- end
-
- def main_object
- @main_object ||= TOPLEVEL_BINDING.eval('self')
- end
-end
diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb
new file mode 100644
index 00000000000..0457e8487d8
--- /dev/null
+++ b/spec/support/redis/redis_helpers.rb
@@ -0,0 +1,18 @@
+module RedisHelpers
+ # config/README.md
+
+ # Usage: performance enhancement
+ def redis_cache_cleanup!
+ Gitlab::Redis::Cache.with(&:flushall)
+ end
+
+ # Usage: SideKiq, Mailroom, CI Runner, Workhorse, push services
+ def redis_queues_cleanup!
+ Gitlab::Redis::Queues.with(&:flushall)
+ end
+
+ # Usage: session state, rate limiting
+ def redis_shared_state_cleanup!
+ Gitlab::Redis::SharedState.with(&:flushall)
+ end
+end
diff --git a/spec/support/routing_helpers.rb b/spec/support/routing_helpers.rb
deleted file mode 100644
index af1f4760804..00000000000
--- a/spec/support/routing_helpers.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-RSpec.configure do |config|
- config.include GitlabRoutingHelper
-end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
new file mode 100644
index 00000000000..dffab22d8b5
--- /dev/null
+++ b/spec/support/rspec.rb
@@ -0,0 +1,12 @@
+require_relative "helpers/stub_configuration"
+require_relative "helpers/stub_object_storage"
+require_relative "helpers/stub_env"
+
+RSpec.configure do |config|
+ config.mock_with :rspec
+ config.raise_errors_for_deprecations!
+
+ config.include StubConfiguration
+ config.include StubObjectStorage
+ config.include StubENV
+end
diff --git a/spec/support/seed.rb b/spec/support/seed.rb
new file mode 100644
index 00000000000..bea2e9c3044
--- /dev/null
+++ b/spec/support/seed.rb
@@ -0,0 +1,7 @@
+RSpec.configure do |config|
+ config.include SeedHelper, :seed_helper
+
+ config.before(:all, :seed_helper) do
+ ensure_seeds
+ end
+end
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
deleted file mode 100644
index 11ef1fc477f..00000000000
--- a/spec/support/seed_helper.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-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
-
-module SeedHelper
- GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __FILE__).freeze
-
- def ensure_seeds
- if File.exist?(SEED_STORAGE_PATH)
- FileUtils.rm_r(SEED_STORAGE_PATH)
- end
-
- FileUtils.mkdir_p(SEED_STORAGE_PATH)
-
- create_bare_seeds
- create_normal_seeds
- create_mutable_seeds
- create_broken_seeds
- create_git_attributes
- create_invalid_git_attributes
- end
-
- def create_bare_seeds
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}),
- chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
- end
-
- def create_normal_seeds
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
- chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
- end
-
- def create_mutable_seeds
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
- chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
-
- mutable_repo_full_path = File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH)
- system(git_env, *%W(#{Gitlab.config.git.bin_path} branch -t feature origin/feature),
- chdir: mutable_repo_full_path, out: '/dev/null', err: '/dev/null')
-
- system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}),
- chdir: mutable_repo_full_path, out: '/dev/null', err: '/dev/null')
- end
-
- def create_broken_seeds
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
- chdir: SEED_STORAGE_PATH,
- out: '/dev/null',
- err: '/dev/null')
-
- refs_path = File.join(SEED_STORAGE_PATH, TEST_BROKEN_REPO_PATH, 'refs')
-
- FileUtils.rm_r(refs_path)
- end
-
- def create_git_attributes
- dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info')
-
- FileUtils.mkdir_p(dir)
-
- File.open(File.join(dir, 'attributes'), 'w') do |handle|
- handle.write <<-EOF.strip
-# This is a comment, it should be ignored.
-
-*.txt text
-*.jpg -text
-*.sh eol=lf gitlab-language=shell
-*.haml.* gitlab-language=haml
-foo/bar.* foo
-*.cgi key=value?p1=v1&p2=v2
-/*.png gitlab-language=png
-*.binary binary
-
-# This uses a tab instead of spaces to ensure the parser also supports this.
-*.md\tgitlab-language=markdown
-bla/bla.txt
- EOF
- end
- end
-
- def create_invalid_git_attributes
- dir = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info')
-
- FileUtils.mkdir_p(dir)
-
- enc = Encoding::UTF_16
-
- File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle|
- handle.write('# hello'.encode(enc))
- end
- end
-
- # Prevent developer git configurations from being persisted to test
- # repositories
- def git_env
- { 'GIT_TEMPLATE_DIR' => '' }
- end
-end
-
-RSpec.configure do |config|
- config.include SeedHelper, :seed_helper
-
- config.before(:all, :seed_helper) do
- ensure_seeds
- end
-end
diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb
new file mode 100644
index 00000000000..43a2fd05498
--- /dev/null
+++ b/spec/support/services/clusters/create_service_shared.rb
@@ -0,0 +1,57 @@
+shared_context 'valid cluster create params' do
+ 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'
+ }
+ }
+ end
+end
+
+shared_context 'invalid cluster create params' do
+ 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'
+ }
+ }
+ end
+end
+
+shared_examples 'create cluster service success' do
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { subject }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+
+ expect(subject.name).to eq('test-cluster')
+ expect(subject.user).to eq(user)
+ expect(subject.project).to eq(project)
+ expect(subject.provider.gcp_project_id).to eq('gcp-project')
+ expect(subject.provider.zone).to eq('us-central1-a')
+ expect(subject.provider.num_nodes).to eq(1)
+ expect(subject.provider.machine_type).to eq('machine_type-a')
+ expect(subject.provider.access_token).to eq(access_token)
+ expect(subject.platform).to be_nil
+ end
+end
+
+shared_examples 'create cluster service error' do
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { subject }.to change { Clusters::Cluster.count }.by(0)
+ expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+end
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index adfd256dff1..1478c6b5a47 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -86,7 +86,7 @@ shared_examples "migrating a deleted user's associated records to the ghost user
end
it "blocks the user before #{record_class_name} migration begins" do
- expect(service).to receive("migrate_#{record_class_name.parameterize('_')}s".to_sym) do
+ expect(service).to receive("migrate_#{record_class_name.parameterize('_').pluralize}".to_sym) do
expect(user.reload).to be_blocked
end
diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/support/shared_contexts/email_shared_blocks.rb
index 9d806fc524d..9d806fc524d 100644
--- a/spec/lib/gitlab/email/email_shared_blocks.rb
+++ b/spec/support/shared_contexts/email_shared_blocks.rb
diff --git a/spec/support/shared_contexts/json_response_shared_context.rb b/spec/support/shared_contexts/json_response_shared_context.rb
new file mode 100644
index 00000000000..df5fc288089
--- /dev/null
+++ b/spec/support/shared_contexts/json_response_shared_context.rb
@@ -0,0 +1,3 @@
+shared_context 'JSON response' do
+ let(:json_response) { JSON.parse(response.body) }
+end
diff --git a/spec/support/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 23f9b46ae0c..23f9b46ae0c 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
index dc97a39f051..dc97a39f051 100644
--- a/spec/support/chat_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
new file mode 100644
index 00000000000..21c6f3c829f
--- /dev/null
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -0,0 +1,741 @@
+shared_examples_for 'common trace features' do
+ describe '#html' do
+ before do
+ trace.set("12\n34")
+ end
+
+ it "returns formatted html" do
+ expect(trace.html).to eq("12<br>34")
+ end
+
+ it "returns last line of formatted html" do
+ expect(trace.html(last_lines: 1)).to eq("34")
+ end
+ end
+
+ describe '#raw' do
+ before do
+ trace.set("12\n34")
+ end
+
+ it "returns raw output" do
+ expect(trace.raw).to eq("12\n34")
+ end
+
+ it "returns last line of raw output" do
+ expect(trace.raw(last_lines: 1)).to eq("34")
+ end
+ end
+
+ describe '#extract_coverage' do
+ let(:regex) { '\(\d+.\d+\%\) covered' }
+
+ context 'matching coverage' do
+ before do
+ trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+ end
+
+ it "returns valid coverage" do
+ expect(trace.extract_coverage(regex)).to eq("98.29")
+ end
+ end
+
+ context 'no coverage' do
+ before do
+ trace.set('No coverage')
+ end
+
+ it 'returs nil' do
+ expect(trace.extract_coverage(regex)).to be_nil
+ end
+ end
+ end
+
+ describe '#extract_sections' do
+ let(:log) { 'No sections' }
+ let(:sections) { trace.extract_sections }
+
+ before do
+ trace.set(log)
+ end
+
+ context 'no sections' do
+ it 'returs []' do
+ expect(trace.extract_sections).to eq([])
+ end
+ end
+
+ context 'multiple sections available' do
+ let(:log) { File.read(expand_fixture_path('trace/trace_with_sections')) }
+ let(:sections_data) do
+ [
+ { name: 'prepare_script', lines: 2, duration: 3.seconds },
+ { name: 'get_sources', lines: 4, duration: 1.second },
+ { name: 'restore_cache', lines: 0, duration: 0.seconds },
+ { name: 'download_artifacts', lines: 0, duration: 0.seconds },
+ { name: 'build_script', lines: 2, duration: 1.second },
+ { name: 'after_script', lines: 0, duration: 0.seconds },
+ { name: 'archive_cache', lines: 0, duration: 0.seconds },
+ { name: 'upload_artifacts', lines: 0, duration: 0.seconds }
+ ]
+ end
+
+ it "returns valid sections" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(sections_data.size),
+ "expected #{sections_data.size} sections, got #{sections.size}"
+
+ buff = StringIO.new(log)
+ sections.each_with_index do |s, i|
+ expected = sections_data[i]
+
+ expect(s[:name]).to eq(expected[:name])
+ expect(s[:date_end] - s[:date_start]).to eq(expected[:duration])
+
+ buff.seek(s[:byte_start], IO::SEEK_SET)
+ length = s[:byte_end] - s[:byte_start]
+ lines = buff.read(length).count("\n")
+ expect(lines).to eq(expected[:lines])
+ end
+ end
+ end
+
+ context 'logs contains "section_start"' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_end:1506417477:a_section\r\033[0K"}
+
+ it "returns only one section" do
+ expect(sections).not_to be_empty
+ expect(sections.size).to eq(1)
+
+ section = sections[0]
+ expect(section[:name]).to eq('a_section')
+ expect(section[:byte_start]).not_to eq(section[:byte_end]), "got an empty section"
+ end
+ end
+
+ context 'missing section_end' do
+ let(:log) { "section_start:1506417476:a_section\r\033[0KSome logs\nNo section_end\n"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'missing section_start' do
+ let(:log) { "Some logs\nNo section_start\nsection_end:1506417476:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+
+ context 'inverted section_start section_end' do
+ let(:log) { "section_end:1506417476:a_section\r\033[0Klooks like a section_start:invalid\nsection_start:1506417477:a_section\r\033[0K"}
+
+ it "returns no sections" do
+ expect(sections).to be_empty
+ end
+ end
+ end
+
+ describe '#set' do
+ before do
+ trace.set("12")
+ end
+
+ it "returns trace" do
+ expect(trace.raw).to eq("12")
+ end
+
+ context 'overwrite trace' do
+ before do
+ trace.set("34")
+ end
+
+ it "returns new trace" do
+ expect(trace.raw).to eq("34")
+ end
+ end
+
+ context 'runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ trace.set(token)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+
+ context 'hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ trace.set(token)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+ end
+
+ describe '#append' do
+ before do
+ trace.set("1234")
+ end
+
+ it "returns correct trace" do
+ expect(trace.append("56", 4)).to eq(6)
+ expect(trace.raw).to eq("123456")
+ end
+
+ context 'tries to append trace at different offset' do
+ it "fails with append" do
+ expect(trace.append("56", 2)).to eq(4)
+ expect(trace.raw).to eq("1234")
+ end
+ end
+
+ context 'runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ trace.append(token, 0)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+
+ context 'build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ trace.append(token, 0)
+ end
+
+ it "hides token" do
+ expect(trace.raw).not_to include(token)
+ end
+ end
+ end
+end
+
+shared_examples_for 'trace with disabled live trace feature' do
+ it_behaves_like 'common trace features'
+
+ describe '#read' do
+ shared_examples 'read successfully with IO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(IO)
+ end
+ end
+ end
+
+ shared_examples 'read successfully with StringIO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(StringIO)
+ end
+ end
+ end
+
+ shared_examples 'failed to read' do
+ it 'yields without source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_nil
+ end
+ end
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when current_path (with project_id) exists' do
+ before do
+ expect(trace).to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when current_path (with project_ci_id) exists' do
+ before do
+ expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when db trace exists' do
+ before do
+ build.send(:write_attribute, :trace, "data")
+ end
+
+ it_behaves_like 'read successfully with StringIO'
+ end
+
+ context 'when no sources exist' do
+ it_behaves_like 'failed to read'
+ end
+ end
+
+ describe 'trace handling' do
+ subject { trace.exist? }
+
+ context 'trace does not exist' do
+ it { expect(trace.exist?).to be(false) }
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the trace artifact has been erased' do
+ before do
+ trace.erase!
+ end
+
+ it { is_expected.to be_falsy }
+
+ it 'removes associations' do
+ expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
+ end
+ end
+ end
+
+ context 'new trace path is used' do
+ before do
+ trace.send(:ensure_directory)
+
+ File.open(trace.send(:default_path), "w") do |file|
+ file.write("data")
+ end
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ end
+ end
+
+ context 'deprecated path' do
+ let(:path) { trace.send(:deprecated_path) }
+
+ context 'with valid ci_id' do
+ before do
+ build.project.update(ci_id: 1000)
+
+ FileUtils.mkdir_p(File.dirname(path))
+
+ File.open(path, "w") do |file|
+ file.write("data")
+ end
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ end
+ end
+
+ context 'without valid ci_id' do
+ it "does not return deprecated path" do
+ expect(path).to be_nil
+ end
+ end
+ end
+
+ context 'stored in database' do
+ before do
+ build.send(:write_attribute, :trace, "data")
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ end
+
+ it "returns database data" do
+ expect(trace.raw).to eq("data")
+ end
+ end
+ end
+
+ describe '#archive!' do
+ subject { trace.archive! }
+
+ shared_examples 'archive trace file' do
+ it do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(1)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ expect(build.job_artifacts_trace.file.filename).to eq('job.log')
+ expect(File.exist?(src_path)).to be_falsy
+ expect(src_checksum)
+ .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
+ end
+ end
+
+ shared_examples 'source trace file stays intact' do |error:|
+ it do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace).to be_nil
+ expect(File.exist?(src_path)).to be_truthy
+ end
+ end
+
+ shared_examples 'archive trace in database' do
+ it do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(1)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ expect(build.job_artifacts_trace.file.filename).to eq('job.log')
+ expect(build.old_trace).to be_nil
+ expect(src_checksum)
+ .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
+ end
+ end
+
+ shared_examples 'source trace in database stays intact' do |error:|
+ it do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace).to be_nil
+ expect(build.old_trace).to eq(trace_content)
+ end
+ end
+
+ context 'when job does not have trace artifact' do
+ context 'when trace file stored in default path' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:src_path) { trace.read { |s| s.path } }
+ let!(:src_checksum) { Digest::SHA256.file(src_path).hexdigest }
+
+ it_behaves_like 'archive trace file'
+
+ context 'when failed to create clone file' do
+ before do
+ allow(IO).to receive(:copy_stream).and_return(0)
+ end
+
+ it_behaves_like 'source trace file stays intact', error: Gitlab::Ci::Trace::ArchiveError
+ end
+
+ context 'when failed to create job artifact record' do
+ before do
+ allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid
+ end
+ end
+
+ context 'when trace is stored in database' do
+ let(:build) { create(:ci_build, :success) }
+ let(:trace_content) { 'Sample trace' }
+ let(:src_checksum) { Digest::SHA256.hexdigest(trace_content) }
+
+ before do
+ build.update_column(:trace, trace_content)
+ end
+
+ it_behaves_like 'archive trace in database'
+
+ context 'when failed to create clone file' do
+ before do
+ allow(IO).to receive(:copy_stream).and_return(0)
+ end
+
+ it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError
+ end
+
+ context 'when failed to create job artifact record' do
+ before do
+ allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
+ end
+
+ context 'when there is a validation error on Ci::Build' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::Build).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ context "when erase old trace with 'save'" do
+ before do
+ build.send(:write_attribute, :trace, nil)
+ build.save
+ end
+
+ it 'old trace is not deleted' do
+ build.reload
+ expect(build.trace.raw).to eq(trace_content)
+ end
+ end
+
+ it_behaves_like 'archive trace in database'
+ end
+ end
+ end
+
+ context 'when job has trace artifact' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Already archived')
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ end
+ end
+
+ context 'when job is not finished yet' do
+ let!(:build) { create(:ci_build, :running, :trace_live) }
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Job is not finished yet')
+ expect(build.trace.exist?).to be_truthy
+ end
+ end
+ end
+end
+
+shared_examples_for 'trace with enabled live trace feature' do
+ it_behaves_like 'common trace features'
+
+ describe '#read' do
+ shared_examples 'read successfully with IO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(IO)
+ end
+ end
+ end
+
+ shared_examples 'read successfully with ChunkedIO' do
+ it 'yields with source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_a(Gitlab::Ci::Trace::ChunkedIO)
+ end
+ end
+ end
+
+ shared_examples 'failed to read' do
+ it 'yields without source' do
+ trace.read do |stream|
+ expect(stream).to be_a(Gitlab::Ci::Trace::Stream)
+ expect(stream.stream).to be_nil
+ end
+ end
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it_behaves_like 'read successfully with IO'
+ end
+
+ context 'when live trace exists' do
+ before do
+ Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream|
+ stream.write('abc')
+ end
+ end
+
+ it_behaves_like 'read successfully with ChunkedIO'
+ end
+
+ context 'when no sources exist' do
+ it_behaves_like 'failed to read'
+ end
+ end
+
+ describe 'trace handling' do
+ subject { trace.exist? }
+
+ context 'trace does not exist' do
+ it { expect(trace.exist?).to be(false) }
+ end
+
+ context 'when trace artifact exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the trace artifact has been erased' do
+ before do
+ trace.erase!
+ end
+
+ it { is_expected.to be_falsy }
+
+ it 'removes associations' do
+ expect(Ci::JobArtifact.exists?(job_id: build.id, file_type: :trace)).to be_falsy
+ end
+ end
+ end
+
+ context 'stored in live trace' do
+ before do
+ Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream|
+ stream.write('abc')
+ end
+ end
+
+ it "trace exist" do
+ expect(trace.exist?).to be(true)
+ end
+
+ it "can be erased" do
+ trace.erase!
+ expect(trace.exist?).to be(false)
+ expect(Ci::BuildTraceChunk.where(build: build)).not_to be_exist
+ end
+
+ it "returns live trace data" do
+ expect(trace.raw).to eq("abc")
+ end
+ end
+ end
+
+ describe '#archive!' do
+ subject { trace.archive! }
+
+ shared_examples 'archive trace file in ChunkedIO' do
+ it do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(1)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ expect(build.job_artifacts_trace.file.filename).to eq('job.log')
+ expect(Ci::BuildTraceChunk.where(build: build)).not_to be_exist
+ expect(src_checksum)
+ .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest)
+ expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum)
+ end
+ end
+
+ shared_examples 'source trace in ChunkedIO stays intact' do |error:|
+ it do
+ expect { subject }.to raise_error(error)
+
+ build.reload
+ expect(build.trace.exist?).to be_truthy
+ expect(build.job_artifacts_trace).to be_nil
+ Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream|
+ expect(stream.read).to eq(trace_raw)
+ end
+ end
+ end
+
+ context 'when job does not have trace artifact' do
+ context 'when trace is stored in ChunkedIO' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:trace_raw) { build.trace.raw }
+ let!(:src_checksum) { Digest::SHA256.hexdigest(trace_raw) }
+
+ it_behaves_like 'archive trace file in ChunkedIO'
+
+ context 'when failed to create clone file' do
+ before do
+ allow(IO).to receive(:copy_stream).and_return(0)
+ end
+
+ it_behaves_like 'source trace in ChunkedIO stays intact', error: Gitlab::Ci::Trace::ArchiveError
+ end
+
+ context 'when failed to create job artifact record' do
+ before do
+ allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ it_behaves_like 'source trace in ChunkedIO stays intact', error: ActiveRecord::RecordInvalid
+ end
+ end
+ end
+
+ context 'when job has trace artifact' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Already archived')
+ expect(build.job_artifacts_trace.file.exists?).to be_truthy
+ end
+ end
+
+ context 'when job is not finished yet' do
+ let!(:build) { create(:ci_build, :running, :trace_live) }
+
+ it 'does not archive' do
+ expect_any_instance_of(described_class).not_to receive(:archive_stream!)
+ expect { subject }.to raise_error('Job is not finished yet')
+ expect(build.trace.exist?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/common_system_notes_examples.rb b/spec/support/shared_examples/common_system_notes_examples.rb
new file mode 100644
index 00000000000..96ef30b7513
--- /dev/null
+++ b/spec/support/shared_examples/common_system_notes_examples.rb
@@ -0,0 +1,27 @@
+shared_examples 'system note creation' do |update_params, note_text|
+ subject { described_class.new(project, user).execute(issuable, [])}
+
+ before do
+ issuable.assign_attributes(update_params)
+ issuable.save
+ end
+
+ it 'creates 1 system note with the correct content' do
+ expect { subject }.to change { Note.count }.from(0).to(1)
+
+ note = Note.last
+ expect(note.note).to match(note_text)
+ expect(note.noteable_type).to eq(issuable.class.name)
+ end
+end
+
+shared_examples 'WIP notes creation' do |wip_action|
+ subject { described_class.new(project, user).execute(issuable, []) }
+
+ it 'creates WIP toggle and title change notes' do
+ expect { subject }.to change { Note.count }.from(0).to(2)
+
+ expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
+ expect(Note.second.note).to match('changed title')
+ end
+end
diff --git a/spec/support/email_format_shared_examples.rb b/spec/support/shared_examples/email_format_shared_examples.rb
index b924a208e71..b924a208e71 100644
--- a/spec/support/email_format_shared_examples.rb
+++ b/spec/support/shared_examples/email_format_shared_examples.rb
diff --git a/spec/support/shared_examples/fast_destroy_all.rb b/spec/support/shared_examples/fast_destroy_all.rb
new file mode 100644
index 00000000000..5448ddcfe33
--- /dev/null
+++ b/spec/support/shared_examples/fast_destroy_all.rb
@@ -0,0 +1,38 @@
+shared_examples_for 'fast destroyable' do
+ describe 'Forbid #destroy and #destroy_all' do
+ it 'does not delete database rows and associted external data' do
+ expect(external_data_counter).to be > 0
+ expect(subjects.count).to be > 0
+
+ expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`')
+ expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`')
+
+ expect(subjects.count).to be > 0
+ expect(external_data_counter).to be > 0
+ end
+ end
+
+ describe '.fast_destroy_all' do
+ it 'deletes database rows and associted external data' do
+ expect(external_data_counter).to be > 0
+ expect(subjects.count).to be > 0
+
+ expect { subjects.fast_destroy_all }.not_to raise_error
+
+ expect(subjects.count).to eq(0)
+ expect(external_data_counter).to eq(0)
+ end
+ end
+
+ describe '.use_fast_destroy' do
+ it 'performs cascading delete with fast_destroy_all' do
+ expect(external_data_counter).to be > 0
+ expect(subjects.count).to be > 0
+
+ expect { parent.destroy }.not_to raise_error
+
+ expect(subjects.count).to eq(0)
+ expect(external_data_counter).to eq(0)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 5b0b609f7f2..5a569d233bc 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -79,7 +79,7 @@ RSpec.shared_examples 'a creatable merge request' do
end
end
- it 'updates the branches when selecting a new target project' do
+ it 'updates the branches when selecting a new target project', :js do
target_project_member = target_project.owner
CreateBranchService.new(target_project, target_project_member)
.execute('a-brand-new-branch-to-test', 'master')
@@ -92,7 +92,7 @@ RSpec.shared_examples 'a creatable merge request' do
first('.js-target-branch').click
- within('.dropdown-target-branch .dropdown-content') do
+ within('.js-target-branch-dropdown .dropdown-content') do
expect(page).to have_content('a-brand-new-branch-to-test')
end
end
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
new file mode 100644
index 00000000000..b29bb3c2fc0
--- /dev/null
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -0,0 +1,52 @@
+RSpec.shared_examples 'Master manages access requests' do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+
+ before do
+ entity.request_access(user)
+ entity.respond_to?(:add_owner) ? entity.add_owner(master) : entity.add_master(master)
+ sign_in(master)
+ end
+
+ it 'master can see access requests' do
+ visit members_page_path
+
+ expect_visible_access_request(entity, user)
+ end
+
+ it 'master can grant access', :js do
+ visit members_page_path
+
+ expect_visible_access_request(entity, user)
+
+ accept_confirm { click_on 'Grant access' }
+
+ expect_no_visible_access_request(entity, user)
+
+ page.within('.members-list') do
+ expect(page).to have_content user.name
+ end
+ end
+
+ it 'master can deny access', :js do
+ visit members_page_path
+
+ expect_visible_access_request(entity, user)
+
+ accept_confirm { click_on 'Deny access' }
+
+ expect_no_visible_access_request(entity, user)
+ expect(page).not_to have_content user.name
+ end
+
+ def expect_visible_access_request(entity, user)
+ expect(entity.requesters.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "Users requesting access to #{entity.name} 1"
+ expect(page).to have_content user.name
+ end
+
+ def expect_no_visible_access_request(entity, user)
+ expect(entity.requesters.exists?(user_id: user)).to be_falsy
+ expect(page).not_to have_content "Users requesting access to #{entity.name}"
+ end
+end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
index 17f319f49e9..5241c0fa6f1 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
@@ -10,7 +10,7 @@ shared_examples "protected branches > access control > CE" do
unless allowed_to_push_button.text == access_type_name
allowed_to_push_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ within(".dropdown.show .dropdown-menu") { click_on access_type_name }
end
end
@@ -55,7 +55,7 @@ shared_examples "protected branches > access control > CE" do
unless allowed_to_merge_button.text == access_type_name
allowed_to_merge_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ within(".dropdown.show .dropdown-menu") { click_on access_type_name }
end
end
diff --git a/spec/support/shared_examples/gitlab_verify.rb b/spec/support/shared_examples/gitlab_verify.rb
new file mode 100644
index 00000000000..560913ca92f
--- /dev/null
+++ b/spec/support/shared_examples/gitlab_verify.rb
@@ -0,0 +1,19 @@
+RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do
+ describe 'batching' do
+ let(:first_batch) { objects[0].id..objects[0].id }
+ let(:second_batch) { objects[1].id..objects[1].id }
+ let(:third_batch) { objects[2].id..objects[2].id }
+
+ it 'iterates through objects in batches' do
+ expect(collect_ranges).to eq([first_batch, second_batch, third_batch])
+ end
+
+ it 'allows the starting ID to be specified' do
+ expect(collect_ranges(start: second_batch.first)).to eq([second_batch, third_batch])
+ end
+
+ it 'allows the finishing ID to be specified' do
+ expect(collect_ranges(finish: second_batch.last)).to eq([first_batch, second_batch])
+ end
+ end
+end
diff --git a/spec/support/group_members_shared_example.rb b/spec/support/shared_examples/group_members_shared_example.rb
index 547c83c7955..547c83c7955 100644
--- a/spec/support/group_members_shared_example.rb
+++ b/spec/support/shared_examples/group_members_shared_example.rb
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
new file mode 100644
index 00000000000..56e86a87ab9
--- /dev/null
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -0,0 +1,19 @@
+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 ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ EOS
+ end
+
+ it 'should return appropriate command' do
+ expect(subject.generate_script).to eq(helm_setup + commands)
+ end
+ end
+end
diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb
index 42f3b4db23c..42f3b4db23c 100644
--- a/spec/support/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/issuable_shared_examples.rb
diff --git a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
new file mode 100644
index 00000000000..f4bc6f8efa5
--- /dev/null
+++ b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
@@ -0,0 +1,62 @@
+shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
+ include ProjectForksHelper
+
+ def get_action(action, project)
+ if action
+ get action, author_id: project.creator.id
+ else
+ get :index, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ def create_issuable(issuable_type, project, source_branch:)
+ if issuable_type == :issue
+ create(issuable_type, project: project, author: project.creator)
+ else
+ create(issuable_type, source_project: project, source_branch: source_branch, author: project.creator)
+ end
+ end
+
+ before do
+ @issuable_ids = %w[fix improve/awesome].map do |source_branch|
+ create_issuable(issuable_type, project, source_branch: source_branch).id
+ end
+ end
+
+ it "creates indexed meta-data object for issuable notes and votes count" do
+ get_action(action, project)
+
+ meta_data = assigns(:issuable_meta_data)
+
+ aggregate_failures do
+ expect(meta_data.keys).to match_array(@issuable_ids)
+ expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta))
+ end
+ end
+
+ it "avoids N+1 queries" do
+ control = ActiveRecord::QueryRecorder.new { get_action(action, project) }
+ issuable = create_issuable(issuable_type, project, source_branch: 'csv')
+
+ if issuable_type == :merge_request
+ issuable.update!(source_project: fork_project(project))
+ end
+
+ expect { get_action(action, project) }.not_to exceed_query_limit(control.count)
+ end
+
+ describe "when given empty collection" do
+ let(:project2) { create(:project, :public) }
+
+ it "doesn't execute any queries with false conditions" do
+ get_empty =
+ if action
+ proc { get action, author_id: project.creator.id }
+ else
+ proc { get :index, namespace_id: project2.namespace, project_id: project2 }
+ end
+
+ expect(&get_empty).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
+ end
+ end
+end
diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/shared_examples/issue_tracker_service_shared_example.rb
index a6ab03cb808..a6ab03cb808 100644
--- a/spec/support/issue_tracker_service_shared_example.rb
+++ b/spec/support/shared_examples/issue_tracker_service_shared_example.rb
diff --git a/spec/support/ldap_shared_examples.rb b/spec/support/shared_examples/ldap_shared_examples.rb
index 52c34e78965..52c34e78965 100644
--- a/spec/support/ldap_shared_examples.rb
+++ b/spec/support/shared_examples/ldap_shared_examples.rb
diff --git a/spec/support/legacy_path_redirect_shared_examples.rb b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb
index f300bdd48b1..f300bdd48b1 100644
--- a/spec/support/legacy_path_redirect_shared_examples.rb
+++ b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb
diff --git a/spec/support/shared_examples/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
new file mode 100644
index 00000000000..65026f1d7c0
--- /dev/null
+++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
@@ -0,0 +1,10 @@
+require 'timeout'
+
+shared_examples 'malicious regexp' do
+ let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' }
+ let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }
+
+ it 'takes under a second' do
+ expect { Timeout.timeout(1) { subject } }.not_to raise_error
+ end
+end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/shared_examples/mentionable_shared_examples.rb
index 1685decbe94..1685decbe94 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/mentionable_shared_examples.rb
diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/shared_examples/milestone_tabs_examples.rb
index 70b499198bf..70b499198bf 100644
--- a/spec/support/milestone_tabs_examples.rb
+++ b/spec/support/shared_examples/milestone_tabs_examples.rb
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
index 144af4fc475..6a6e13418a9 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_spec.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
@@ -19,6 +19,14 @@ shared_examples_for 'AtomicInternalId' do
it { is_expected.to validate_numericality_of(internal_id_attribute) }
end
+ describe 'Creating an instance' do
+ subject { instance.save! }
+
+ it 'saves a new instance properly' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
describe 'internal id generation' do
subject { instance.save! }
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
new file mode 100644
index 00000000000..76611e54306
--- /dev/null
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -0,0 +1,63 @@
+RSpec.shared_examples 'members notifications' do |entity_type|
+ let(:notification_service) { double('NotificationService').as_null_object }
+
+ before do
+ allow(member).to receive(:notification_service).and_return(notification_service)
+ end
+
+ describe "#after_create" do
+ let(:member) { build(:"#{entity_type}_member") }
+
+ it "sends email to user" do
+ expect(notification_service).to receive(:"new_#{entity_type}_member").with(member)
+
+ member.save
+ end
+ end
+
+ describe "#after_update" do
+ let(:member) { create(:"#{entity_type}_member", :developer) }
+
+ it "calls NotificationService.update_#{entity_type}_member" do
+ expect(notification_service).to receive(:"update_#{entity_type}_member").with(member)
+
+ member.update_attribute(:access_level, Member::MASTER)
+ end
+
+ it "does not send an email when the access level has not changed" do
+ expect(notification_service).not_to receive(:"update_#{entity_type}_member")
+
+ member.touch
+ end
+ end
+
+ describe '#accept_request' do
+ let(:member) { create(:"#{entity_type}_member", :access_request) }
+
+ it "calls NotificationService.new_#{entity_type}_member" do
+ expect(notification_service).to receive(:"new_#{entity_type}_member").with(member)
+
+ member.accept_request
+ end
+ end
+
+ describe "#accept_invite!" do
+ let(:member) { create(:"#{entity_type}_member", :invited) }
+
+ it "calls NotificationService.accept_#{entity_type}_invite" do
+ expect(notification_service).to receive(:"accept_#{entity_type}_invite").with(member)
+
+ member.accept_invite!(build(:user))
+ end
+ end
+
+ describe "#decline_invite!" do
+ let(:member) { create(:"#{entity_type}_member", :invited) }
+
+ it "calls NotificationService.decline_#{entity_type}_invite" do
+ expect(notification_service).to receive(:"decline_#{entity_type}_invite").with(member)
+
+ member.decline_invite!
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
new file mode 100644
index 00000000000..47ad0c6345d
--- /dev/null
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+shared_examples_for 'model with mounted uploader' do |supports_fileuploads|
+ describe '.destroy' do
+ before do
+ stub_uploads_object_storage(uploader_class)
+
+ model_object.public_send(upload_attribute).migrate!(ObjectStorage::Store::REMOTE)
+ end
+
+ it 'deletes remote uploads' do
+ expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
+
+ expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ end
+
+ it 'deletes any FileUploader uploads which are not mounted', skip: !supports_fileuploads do
+ create(:upload, uploader: FileUploader, model: model_object)
+
+ expect { model_object.destroy }.to change { Upload.count }.by(-2)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb
new file mode 100644
index 00000000000..43fdaddf545
--- /dev/null
+++ b/spec/support/shared_examples/notify_shared_examples.rb
@@ -0,0 +1,231 @@
+shared_context 'gitlab email notification' do
+ set(:project) { create(:project, :repository) }
+ set(:recipient) { create(:user, email: 'recipient@example.com') }
+
+ let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
+ let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
+ let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
+ let(:new_user_address) { 'newguy@example.com' }
+
+ before do
+ email = recipient.emails.create(email: "notifications@example.com")
+ recipient.update_attribute(:notification_email, email.email)
+ stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
+ end
+end
+
+shared_context 'reply-by-email is enabled with incoming address without %{key}' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: "reply@#{Gitlab.config.gitlab.host}")
+ end
+end
+
+shared_examples 'a multiple recipients email' do
+ it 'is sent to the given recipient' do
+ is_expected.to deliver_to recipient.notification_email
+ end
+end
+
+shared_examples 'an email sent from GitLab' do
+ it 'has the characteristics of an email sent from GitLab' do
+ sender = subject.header[:from].addrs[0]
+ reply_to = subject.header[:reply_to].addresses
+
+ aggregate_failures do
+ expect(sender.display_name).to eq(gitlab_sender_display_name)
+ expect(sender.address).to eq(gitlab_sender)
+ expect(reply_to).to eq([gitlab_sender_reply_to])
+ end
+ end
+end
+
+shared_examples 'an email that contains a header with author username' do
+ it 'has X-GitLab-Author header containing author\'s username' do
+ is_expected.to have_header 'X-GitLab-Author', user.username
+ end
+end
+
+shared_examples 'an email with X-GitLab headers containing project details' do
+ it 'has X-GitLab-Project headers' do
+ aggregate_failures do
+ is_expected.to have_header('X-GitLab-Project', /#{project.name}/)
+ is_expected.to have_header('X-GitLab-Project-Id', /#{project.id}/)
+ is_expected.to have_header('X-GitLab-Project-Path', /#{project.full_path}/)
+ end
+ end
+end
+
+shared_examples 'a new thread email with reply-by-email enabled' do
+ it 'has the characteristics of a threaded email' do
+ host = Gitlab.config.gitlab.host
+ route_key = "#{model.class.model_name.singular_route_key}_#{model.id}"
+
+ aggregate_failures do
+ is_expected.to have_header('Message-ID', "<#{route_key}@#{host}>")
+ is_expected.to have_header('References', /\A<reply\-.*@#{host}>\Z/ )
+ end
+ end
+end
+
+shared_examples 'a thread answer email with reply-by-email enabled' do
+ include_examples 'an email with X-GitLab headers containing project details'
+
+ it 'has the characteristics of a threaded reply' do
+ host = Gitlab.config.gitlab.host
+ route_key = "#{model.class.model_name.singular_route_key}_#{model.id}"
+
+ aggregate_failures do
+ is_expected.to have_header('Message-ID', /\A<.*@#{host}>\Z/)
+ is_expected.to have_header('In-Reply-To', "<#{route_key}@#{host}>")
+ is_expected.to have_header('References', /\A<#{route_key}@#{host}> <reply\-.*@#{host}>\Z/ )
+ is_expected.to have_subject(/^Re: /)
+ end
+ end
+end
+
+shared_examples 'an email starting a new thread with reply-by-email enabled' do
+ include_examples 'an email with X-GitLab headers containing project details'
+ include_examples 'a new thread email with reply-by-email enabled'
+
+ context 'when reply-by-email is enabled with incoming address with %{key}' do
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+
+ context 'when reply-by-email is enabled with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+ include_examples 'a new thread email with reply-by-email enabled'
+
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+end
+
+shared_examples 'an answer to an existing thread with reply-by-email enabled' do
+ include_examples 'an email with X-GitLab headers containing project details'
+ include_examples 'a thread answer email with reply-by-email enabled'
+
+ context 'when reply-by-email is enabled with incoming address with %{key}' do
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+
+ context 'when reply-by-email is enabled with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+ include_examples 'a thread answer email with reply-by-email enabled'
+
+ it 'has a Reply-To header' do
+ is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
+ end
+ end
+end
+
+shared_examples 'it should have Gmail Actions links' do
+ it do
+ aggregate_failures do
+ is_expected.to have_body_text('<script type="application/ld+json">')
+ is_expected.to have_body_text('ViewAction')
+ end
+ end
+end
+
+shared_examples 'it should not have Gmail Actions links' do
+ it do
+ aggregate_failures do
+ is_expected.not_to have_body_text('<script type="application/ld+json">')
+ is_expected.not_to have_body_text('ViewAction')
+ end
+ end
+end
+
+shared_examples 'it should show Gmail Actions View Issue link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text('View Issue') }
+end
+
+shared_examples 'it should show Gmail Actions View Merge request link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text('View Merge request') }
+end
+
+shared_examples 'it should show Gmail Actions View Commit link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text('View Commit') }
+end
+
+shared_examples 'an unsubscribeable thread' do
+ it_behaves_like 'an unsubscribeable thread with incoming address without %{key}'
+
+ it 'has a List-Unsubscribe header in the correct format, and a body link' do
+ aggregate_failures do
+ is_expected.to have_header('List-Unsubscribe', /unsubscribe/)
+ is_expected.to have_header('List-Unsubscribe', /mailto/)
+ is_expected.to have_header('List-Unsubscribe', /^<.+,.+>$/)
+ is_expected.to have_body_text('unsubscribe')
+ end
+ end
+end
+
+shared_examples 'an unsubscribeable thread with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+
+ it 'has a List-Unsubscribe header in the correct format, and a body link' do
+ aggregate_failures do
+ is_expected.to have_header('List-Unsubscribe', /unsubscribe/)
+ is_expected.not_to have_header('List-Unsubscribe', /mailto/)
+ is_expected.to have_header('List-Unsubscribe', /^<[^,]+>$/)
+ is_expected.to have_body_text('unsubscribe')
+ end
+ end
+end
+
+shared_examples 'a user cannot unsubscribe through footer link' do
+ it 'does not have a List-Unsubscribe header or a body link' do
+ aggregate_failures do
+ is_expected.not_to have_header('List-Unsubscribe', /unsubscribe/)
+ is_expected.not_to have_body_text('unsubscribe')
+ end
+ end
+end
+
+shared_examples 'an email with a labels subscriptions link in its footer' do
+ it { is_expected.to have_body_text('label subscriptions') }
+end
+
+shared_examples 'a note email' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it 'is sent to the given recipient as the author' do
+ sender = subject.header[:from].addrs[0]
+
+ aggregate_failures do
+ expect(sender.display_name).to eq(note_author.name)
+ expect(sender.address).to eq(gitlab_sender)
+ expect(subject).to deliver_to(recipient.notification_email)
+ end
+ end
+
+ it 'contains the message from the note' do
+ is_expected.to have_html_escaped_body_text note.note
+ end
+
+ it 'does not contain note author' do
+ is_expected.not_to have_body_text note.author_name
+ end
+
+ context 'when enabled email_author_in_body' do
+ before do
+ stub_application_setting(email_author_in_body: true)
+ end
+
+ it 'contains a link to note author' do
+ is_expected.to have_html_escaped_body_text note.author_name
+ end
+ end
+end
diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/shared_examples/reference_parser_shared_examples.rb
index baf8bcc04b8..baf8bcc04b8 100644
--- a/spec/support/reference_parser_shared_examples.rb
+++ b/spec/support/shared_examples/reference_parser_shared_examples.rb
diff --git a/spec/support/shared_examples/requests/api/diff_discussions.rb b/spec/support/shared_examples/requests/api/diff_discussions.rb
new file mode 100644
index 00000000000..85a4bd8ca27
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/diff_discussions.rb
@@ -0,0 +1,57 @@
+shared_examples 'diff discussions API' do |parent_type, noteable_type, id_name|
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
+ it "includes diff discussions" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user)
+
+ discussion = json_response.find { |record| record['id'] == diff_note.discussion_id }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(discussion).not_to be_nil
+ expect(discussion['individual_note']).to eq(false)
+ expect(discussion['notes'].first['body']).to eq(diff_note.note)
+ end
+ end
+
+ describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id" do
+ it "returns a discussion by id" do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{diff_note.discussion_id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(diff_note.discussion_id)
+ expect(json_response['notes'].first['body']).to eq(diff_note.note)
+ expect(json_response['notes'].first['position']).to eq(diff_note.position.to_h.stringify_keys)
+ end
+ end
+
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
+ it "creates a new diff note" do
+ position = diff_note.position.to_h
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), body: 'hi!', position: position
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['notes'].first['body']).to eq('hi!')
+ expect(json_response['notes'].first['type']).to eq('DiffNote')
+ expect(json_response['notes'].first['position']).to eq(position.stringify_keys)
+ end
+
+ it "returns a 400 bad request error when position is invalid" do
+ position = diff_note.position.to_h.merge(new_line: '100000')
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), body: 'hi!', position: position
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes" do
+ it 'adds a new note to the diff discussion' do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{diff_note.discussion_id}/notes", user), body: 'hi!'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['type']).to eq('DiffNote')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions.rb b/spec/support/shared_examples/requests/api/resolvable_discussions.rb
new file mode 100644
index 00000000000..408ad08cc48
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/resolvable_discussions.rb
@@ -0,0 +1,87 @@
+shared_examples 'resolvable discussions API' do |parent_type, noteable_type, id_name|
+ describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id" do
+ it "resolves discussion if resolved is true" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", user), resolved: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['notes'].size).to eq(1)
+ expect(json_response['notes'][0]['resolved']).to eq(true)
+ end
+
+ it "unresolves discussion if resolved is false" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", user), resolved: false
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['notes'].size).to eq(1)
+ expect(json_response['notes'][0]['resolved']).to eq(false)
+ end
+
+ it "returns a 400 bad request error if resolved parameter is not passed" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "returns a 401 unauthorized error if user is not authenticated" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}"), resolved: true
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+
+ it "returns a 403 error if user resolves discussion of someone else" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", private_user), resolved: true
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ context 'when user does not have access to read the discussion' do
+ before do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'responds with 404' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}", private_user), resolved: true
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ it 'returns resolved note when resolved parameter is true' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user), resolved: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['resolved']).to eq(true)
+ end
+
+ it 'returns a 404 error when note id not found' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/12345", user),
+ body: 'Hello!'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns a 400 bad request error if neither body nor resolved parameter is given' do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it "returns a 403 error if user resolves note of someone else" do
+ put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes/#{note.id}", private_user), resolved: true
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
new file mode 100644
index 00000000000..2228e872926
--- /dev/null
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -0,0 +1,429 @@
+Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
+
+RSpec.shared_examples 'slack or mattermost notifications' do
+ let(:chat_service) { described_class.new }
+ let(:webhook_url) { 'https://example.gitlab.com/' }
+
+ def execute_with_options(options)
+ receive(:new).with(webhook_url, options)
+ .and_return(double(:slack_service).as_null_object)
+ end
+
+ 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, :wiki_repo) }
+ let(:username) { 'slack_username' }
+ let(:channel) { 'slack_channel' }
+ let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } }
+
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+
+ issue_service = Issues::CreateService.new(project, user, issue_service_options)
+ @issue = issue_service.execute
+ @issues_sample_data = issue_service.hook_data(@issue, 'open')
+
+ project.add_developer(user)
+ opts = {
+ title: 'Awesome merge_request',
+ description: 'please fix',
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ merge_service = MergeRequests::CreateService.new(project,
+ user, opts)
+ @merge_request = merge_service.execute
+ @merge_sample_data = merge_service.hook_data(@merge_request,
+ 'open')
+
+ opts = {
+ title: "Awesome wiki_page",
+ content: "Some text describing some thing or another",
+ format: "md",
+ message: "user created page: Awesome wiki_page"
+ }
+
+ @wiki_page = create(:wiki_page, wiki: project.wiki, attrs: opts)
+ @wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create')
+ end
+
+ it "calls Slack/Mattermost API for push events" do
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for issue events" do
+ chat_service.execute(@issues_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for merge requests events" do
+ chat_service.execute(@merge_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for wiki page events" do
+ chat_service.execute(@wiki_page_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it 'uses the username as an option for slack when configured' do
+ allow(chat_service).to receive(:username).and_return(username)
+
+ expect(Slack::Notifier).to receive(:new)
+ .with(webhook_url, username: username)
+ .and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(push_sample_data)
+ end
+
+ it 'uses the channel as an option when it is configured' do
+ allow(chat_service).to receive(:channel).and_return(channel)
+ expect(Slack::Notifier).to receive(:new)
+ .with(webhook_url, channel: channel)
+ .and_return(
+ double(:slack_service).as_null_object
+ )
+ chat_service.execute(push_sample_data)
+ end
+
+ context "event channels" do
+ it "uses the right channel for push event" do
+ chat_service.update_attributes(push_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new)
+ .with(webhook_url, channel: "random")
+ .and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(push_sample_data)
+ end
+
+ it "uses the right channel for merge request event" do
+ chat_service.update_attributes(merge_request_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new)
+ .with(webhook_url, channel: "random")
+ .and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@merge_sample_data)
+ end
+
+ it "uses the right channel for issue event" do
+ chat_service.update_attributes(issue_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new)
+ .with(webhook_url, channel: "random")
+ .and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@issues_sample_data)
+ end
+
+ context 'for confidential issues' do
+ let(:issue_service_options) { { title: 'Secret', confidential: true } }
+
+ it "uses confidential issue channel" do
+ chat_service.update_attributes(confidential_issue_channel: 'confidential')
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
+
+ chat_service.execute(@issues_sample_data)
+ end
+
+ it 'falls back to issue channel' do
+ chat_service.update_attributes(issue_channel: 'fallback_channel')
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
+
+ chat_service.execute(@issues_sample_data)
+ end
+ end
+
+ it "uses the right channel for wiki event" do
+ chat_service.update_attributes(wiki_page_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new)
+ .with(webhook_url, channel: "random")
+ .and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@wiki_page_sample_data)
+ end
+
+ context "note event" do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "uses the right channel" do
+ chat_service.update_attributes(note_channel: "random")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to receive(:new)
+ .with(webhook_url, channel: "random")
+ .and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(note_data)
+ end
+
+ context 'for confidential notes' do
+ before do
+ issue_note.noteable.update!(confidential: true)
+ end
+
+ it "uses confidential channel" do
+ chat_service.update_attributes(confidential_note_channel: "confidential")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
+
+ chat_service.execute(note_data)
+ end
+
+ it 'falls back to note channel' do
+ chat_service.update_attributes(note_channel: "fallback_channel")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
+
+ chat_service.execute(note_data)
+ end
+ end
+ end
+ end
+ end
+
+ describe "Note events" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, creator: user) }
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ context 'when commit comment event executed' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "calls Slack/Mattermost API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when merge request comment event executed' do
+ let(:merge_request_note) do
+ create(:note_on_merge_request, project: project,
+ note: "merge request note")
+ end
+
+ it "calls Slack API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when issue comment event executed' do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
+
+ it "calls Slack API for issue comment events" do
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when snippet comment event executed' do
+ let(:snippet_note) do
+ create(:note_on_project_snippet, project: project,
+ note: "snippet note")
+ end
+
+ it "calls Slack API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+ end
+
+ describe 'Pipeline events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: status,
+ sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+ end
+
+ shared_examples 'call Slack/Mattermost API' do
+ before do
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it 'calls Slack/Mattermost API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
+
+ context 'with succeeded pipeline' do
+ let(:status) { 'success' }
+
+ context 'with default to notify_only_broken_pipelines' do
+ it 'does not call Slack/Mattermost API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = chat_service.execute(data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context 'with setting notify_only_broken_pipelines to false' do
+ before do
+ chat_service.notify_only_broken_pipelines = false
+ end
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
+ end
+
+ context 'only notify for the default branch' do
+ context 'when enabled' do
+ let(:pipeline) do
+ create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
+ end
+
+ before do
+ chat_service.notify_only_default_branch = true
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it 'does not call the Slack/Mattermost API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = chat_service.execute(data)
+
+ expect(result).to be_falsy
+ end
+
+ it 'does not notify push events if they are not for the default branch' do
+ ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
+ push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).not_to have_requested(:post, webhook_url)
+ end
+
+ it 'notifies about push events for the default branch' do
+ push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when disabled' do
+ let(:pipeline) do
+ create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
+ end
+
+ before do
+ chat_service.notify_only_default_branch = false
+ end
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
+ end
+ end
+end
diff --git a/spec/support/snippet_visibility.rb b/spec/support/shared_examples/snippet_visibility.rb
index 3a7c69b7877..3a7c69b7877 100644
--- a/spec/support/snippet_visibility.rb
+++ b/spec/support/shared_examples/snippet_visibility.rb
diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/shared_examples/snippets_shared_examples.rb
index 85f0facd5c3..85f0facd5c3 100644
--- a/spec/support/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/snippets_shared_examples.rb
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/shared_examples/taskable_shared_examples.rb
index 4056ff06b84..4056ff06b84 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/shared_examples/taskable_shared_examples.rb
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/shared_examples/time_tracking_shared_examples.rb
index 909d4e2ee8d..909d4e2ee8d 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/time_tracking_shared_examples.rb
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/shared_examples/unique_ip_check_shared_examples.rb
index e5c8ac6a004..e5c8ac6a004 100644
--- a/spec/support/unique_ip_check_shared_examples.rb
+++ b/spec/support/shared_examples/unique_ip_check_shared_examples.rb
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/shared_examples/update_invalid_issuable.rb
index 1490287681b..1490287681b 100644
--- a/spec/support/update_invalid_issuable.rb
+++ b/spec/support/shared_examples/update_invalid_issuable.rb
diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/shared_examples/updating_mentions_shared_examples.rb
index 5e3f19ba19e..5e3f19ba19e 100644
--- a/spec/support/updating_mentions_shared_examples.rb
+++ b/spec/support/shared_examples/updating_mentions_shared_examples.rb
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
deleted file mode 100644
index 07bc3a51fd8..00000000000
--- a/spec/support/slack_mattermost_notifications_shared_examples.rb
+++ /dev/null
@@ -1,429 +0,0 @@
-Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
-
-RSpec.shared_examples 'slack or mattermost notifications' do
- let(:chat_service) { described_class.new }
- let(:webhook_url) { 'https://example.gitlab.com/' }
-
- def execute_with_options(options)
- receive(:new).with(webhook_url, options)
- .and_return(double(:slack_service).as_null_object)
- end
-
- 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(:username) { 'slack_username' }
- let(:channel) { 'slack_channel' }
- let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } }
-
- let(:push_sample_data) do
- Gitlab::DataBuilder::Push.build_sample(project, user)
- end
-
- before do
- allow(chat_service).to receive_messages(
- project: project,
- project_id: project.id,
- service_hook: true,
- webhook: webhook_url
- )
-
- WebMock.stub_request(:post, webhook_url)
-
- issue_service = Issues::CreateService.new(project, user, issue_service_options)
- @issue = issue_service.execute
- @issues_sample_data = issue_service.hook_data(@issue, 'open')
-
- project.add_developer(user)
- opts = {
- title: 'Awesome merge_request',
- description: 'please fix',
- source_branch: 'feature',
- target_branch: 'master'
- }
- merge_service = MergeRequests::CreateService.new(project,
- user, opts)
- @merge_request = merge_service.execute
- @merge_sample_data = merge_service.hook_data(@merge_request,
- 'open')
-
- opts = {
- title: "Awesome wiki_page",
- content: "Some text describing some thing or another",
- format: "md",
- message: "user created page: Awesome wiki_page"
- }
-
- @wiki_page = create(:wiki_page, wiki: project.wiki, attrs: opts)
- @wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create')
- end
-
- it "calls Slack/Mattermost API for push events" do
- chat_service.execute(push_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it "calls Slack/Mattermost API for issue events" do
- chat_service.execute(@issues_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it "calls Slack/Mattermost API for merge requests events" do
- chat_service.execute(@merge_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it "calls Slack/Mattermost API for wiki page events" do
- chat_service.execute(@wiki_page_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
-
- it 'uses the username as an option for slack when configured' do
- allow(chat_service).to receive(:username).and_return(username)
-
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, username: username)
- .and_return(
- double(:slack_service).as_null_object
- )
-
- chat_service.execute(push_sample_data)
- end
-
- it 'uses the channel as an option when it is configured' do
- allow(chat_service).to receive(:channel).and_return(channel)
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: channel)
- .and_return(
- double(:slack_service).as_null_object
- )
- chat_service.execute(push_sample_data)
- end
-
- context "event channels" do
- it "uses the right channel for push event" do
- chat_service.update_attributes(push_channel: "random")
-
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random")
- .and_return(
- double(:slack_service).as_null_object
- )
-
- chat_service.execute(push_sample_data)
- end
-
- it "uses the right channel for merge request event" do
- chat_service.update_attributes(merge_request_channel: "random")
-
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random")
- .and_return(
- double(:slack_service).as_null_object
- )
-
- chat_service.execute(@merge_sample_data)
- end
-
- it "uses the right channel for issue event" do
- chat_service.update_attributes(issue_channel: "random")
-
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random")
- .and_return(
- double(:slack_service).as_null_object
- )
-
- chat_service.execute(@issues_sample_data)
- end
-
- context 'for confidential issues' do
- let(:issue_service_options) { { title: 'Secret', confidential: true } }
-
- it "uses confidential issue channel" do
- chat_service.update_attributes(confidential_issue_channel: 'confidential')
-
- expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
-
- chat_service.execute(@issues_sample_data)
- end
-
- it 'falls back to issue channel' do
- chat_service.update_attributes(issue_channel: 'fallback_channel')
-
- expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
-
- chat_service.execute(@issues_sample_data)
- end
- end
-
- it "uses the right channel for wiki event" do
- chat_service.update_attributes(wiki_page_channel: "random")
-
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random")
- .and_return(
- double(:slack_service).as_null_object
- )
-
- chat_service.execute(@wiki_page_sample_data)
- end
-
- context "note event" do
- let(:issue_note) do
- create(:note_on_issue, project: project, note: "issue note")
- end
-
- it "uses the right channel" do
- chat_service.update_attributes(note_channel: "random")
-
- note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
-
- expect(Slack::Notifier).to receive(:new)
- .with(webhook_url, channel: "random")
- .and_return(
- double(:slack_service).as_null_object
- )
-
- chat_service.execute(note_data)
- end
-
- context 'for confidential notes' do
- before do
- issue_note.noteable.update!(confidential: true)
- end
-
- it "uses confidential channel" do
- chat_service.update_attributes(confidential_note_channel: "confidential")
-
- note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
-
- expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
-
- chat_service.execute(note_data)
- end
-
- it 'falls back to note channel' do
- chat_service.update_attributes(note_channel: "fallback_channel")
-
- note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
-
- expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
-
- chat_service.execute(note_data)
- end
- end
- end
- end
- end
-
- describe "Note events" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, creator: user) }
-
- before do
- allow(chat_service).to receive_messages(
- project: project,
- project_id: project.id,
- service_hook: true,
- webhook: webhook_url
- )
-
- WebMock.stub_request(:post, webhook_url)
- end
-
- context 'when commit comment event executed' do
- let(:commit_note) do
- create(:note_on_commit, author: user,
- project: project,
- commit_id: project.repository.commit.id,
- note: 'a comment on a commit')
- end
-
- it "calls Slack/Mattermost API for commit comment events" do
- data = Gitlab::DataBuilder::Note.build(commit_note, user)
- chat_service.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'when merge request comment event executed' do
- let(:merge_request_note) do
- create(:note_on_merge_request, project: project,
- note: "merge request note")
- end
-
- it "calls Slack API for merge request comment events" do
- data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
- chat_service.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'when issue comment event executed' do
- let(:issue_note) do
- create(:note_on_issue, project: project, note: "issue note")
- end
-
- let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
-
- it "calls Slack API for issue comment events" do
- chat_service.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'when snippet comment event executed' do
- let(:snippet_note) do
- create(:note_on_project_snippet, project: project,
- note: "snippet note")
- end
-
- it "calls Slack API for snippet comment events" do
- data = Gitlab::DataBuilder::Note.build(snippet_note, user)
- chat_service.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
- end
-
- describe 'Pipeline events' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- let(:pipeline) do
- create(:ci_pipeline,
- project: project, status: status,
- sha: project.commit.sha, ref: project.default_branch)
- end
-
- before do
- allow(chat_service).to receive_messages(
- project: project,
- service_hook: true,
- webhook: webhook_url
- )
- end
-
- shared_examples 'call Slack/Mattermost API' do
- before do
- WebMock.stub_request(:post, webhook_url)
- end
-
- it 'calls Slack/Mattermost API for pipeline events' do
- data = Gitlab::DataBuilder::Pipeline.build(pipeline)
- chat_service.execute(data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'with failed pipeline' do
- let(:status) { 'failed' }
-
- it_behaves_like 'call Slack/Mattermost API'
- end
-
- context 'with succeeded pipeline' do
- let(:status) { 'success' }
-
- context 'with default to notify_only_broken_pipelines' do
- it 'does not call Slack/Mattermost API for pipeline events' do
- data = Gitlab::DataBuilder::Pipeline.build(pipeline)
- result = chat_service.execute(data)
-
- expect(result).to be_falsy
- end
- end
-
- context 'with setting notify_only_broken_pipelines to false' do
- before do
- chat_service.notify_only_broken_pipelines = false
- end
-
- it_behaves_like 'call Slack/Mattermost API'
- end
- end
-
- context 'only notify for the default branch' do
- context 'when enabled' do
- let(:pipeline) do
- create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
- end
-
- before do
- chat_service.notify_only_default_branch = true
- WebMock.stub_request(:post, webhook_url)
- end
-
- it 'does not call the Slack/Mattermost API for pipeline events' do
- data = Gitlab::DataBuilder::Pipeline.build(pipeline)
- result = chat_service.execute(data)
-
- expect(result).to be_falsy
- end
-
- it 'does not notify push events if they are not for the default branch' do
- ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
-
- chat_service.execute(push_sample_data)
-
- expect(WebMock).not_to have_requested(:post, webhook_url)
- end
-
- it 'notifies about push events for the default branch' do
- push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
-
- chat_service.execute(push_sample_data)
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-
- context 'when disabled' do
- let(:pipeline) do
- create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
- end
-
- before do
- chat_service.notify_only_default_branch = false
- end
-
- it_behaves_like 'call Slack/Mattermost API'
- end
- end
- end
-end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
deleted file mode 100644
index a75a3eaefcb..00000000000
--- a/spec/support/stub_configuration.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-module StubConfiguration
- def stub_application_setting(messages)
- add_predicates(messages)
-
- # Stubbing both of these because we're not yet consistent with how we access
- # current application settings
- allow_any_instance_of(ApplicationSetting).to receive_messages(to_settings(messages))
- allow(Gitlab::CurrentSettings.current_application_settings)
- .to receive_messages(to_settings(messages))
-
- # Ensure that we don't use the Markdown cache when stubbing these values
- allow_any_instance_of(ApplicationSetting).to receive(:cached_html_up_to_date?).and_return(false)
- end
-
- def stub_not_protect_default_branch
- stub_application_setting(
- default_branch_protection: Gitlab::Access::PROTECTION_NONE)
- end
-
- def stub_config_setting(messages)
- allow(Gitlab.config.gitlab).to receive_messages(to_settings(messages))
- end
-
- def stub_gravatar_setting(messages)
- allow(Gitlab.config.gravatar).to receive_messages(to_settings(messages))
- end
-
- def stub_incoming_email_setting(messages)
- allow(Gitlab.config.incoming_email).to receive_messages(to_settings(messages))
- end
-
- def stub_mattermost_setting(messages)
- allow(Gitlab.config.mattermost).to receive_messages(to_settings(messages))
- end
-
- def stub_omniauth_setting(messages)
- allow(Gitlab.config.omniauth).to receive_messages(to_settings(messages))
- end
-
- def stub_backup_setting(messages)
- allow(Gitlab.config.backup).to receive_messages(to_settings(messages))
- end
-
- def stub_lfs_setting(messages)
- allow(Gitlab.config.lfs).to receive_messages(to_settings(messages))
- end
-
- def stub_artifacts_setting(messages)
- allow(Gitlab.config.artifacts).to receive_messages(to_settings(messages))
- end
-
- def stub_storage_settings(messages)
- messages.deep_stringify_keys!
-
- # Default storage is always required
- messages['default'] ||= Gitlab.config.repositories.storages.default
- messages.each do |storage_name, storage_hash|
- if !storage_hash.key?('path') || storage_hash['path'] == Gitlab::GitalyClient::StorageSettings::Deprecated
- storage_hash['path'] = TestEnv.repos_path
- end
-
- messages[storage_name] = Gitlab::GitalyClient::StorageSettings.new(storage_hash.to_h)
- end
-
- allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
- end
-
- private
-
- # Modifies stubbed messages to also stub possible predicate versions
- #
- # Examples:
- #
- # add_predicates(foo: true)
- # # => {foo: true, foo?: true}
- #
- # add_predicates(signup_enabled?: false)
- # # => {signup_enabled? false}
- def add_predicates(messages)
- # Only modify keys that aren't already predicates
- keys = messages.keys.map(&:to_s).reject { |k| k.end_with?('?') }
-
- keys.each do |key|
- predicate = key + '?'
- messages[predicate.to_sym] = messages[key.to_sym]
- end
- end
-
- # Support nested hashes by converting all values into Settingslogic objects
- def to_settings(hash)
- hash.transform_values do |value|
- if value.is_a? Hash
- Settingslogic.new(value.deep_stringify_keys)
- else
- value
- end
- end
- end
-end
diff --git a/spec/support/stub_object_storage.rb b/spec/support/stub_object_storage.rb
deleted file mode 100644
index 6e88641da42..00000000000
--- a/spec/support/stub_object_storage.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module StubConfiguration
- def stub_object_storage_uploader(
- config:,
- uploader:,
- remote_directory:,
- enabled: true,
- proxy_download: false,
- background_upload: false,
- direct_upload: false
- )
- allow(config).to receive(:enabled) { enabled }
- allow(config).to receive(:proxy_download) { proxy_download }
- allow(config).to receive(:background_upload) { background_upload }
- allow(config).to receive(:direct_upload) { direct_upload }
-
- return unless enabled
-
- Fog.mock!
-
- ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
- begin
- connection.directories.create(key: remote_directory)
- rescue Excon::Error::Conflict
- end
- end
- end
-
- def stub_artifacts_object_storage(**params)
- stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
- uploader: JobArtifactUploader,
- remote_directory: 'artifacts',
- **params)
- end
-
- def stub_lfs_object_storage(**params)
- stub_object_storage_uploader(config: Gitlab.config.lfs.object_store,
- uploader: LfsObjectUploader,
- remote_directory: 'lfs-objects',
- **params)
- end
-
- def stub_uploads_object_storage(uploader = described_class, **params)
- stub_object_storage_uploader(config: Gitlab.config.uploads.object_store,
- uploader: uploader,
- remote_directory: 'uploads',
- **params)
- end
-end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
deleted file mode 100644
index d87f265cdf0..00000000000
--- a/spec/support/test_env.rb
+++ /dev/null
@@ -1,366 +0,0 @@
-require 'rspec/mocks'
-require 'toml-rb'
-
-module TestEnv
- extend self
-
- ComponentFailedToInstallError = Class.new(StandardError)
-
- # When developing the seed repository, comment out the branch you will modify.
- BRANCH_SHA = {
- 'signed-commits' => '2d1096e',
- 'not-merged-branch' => 'b83d6e3',
- 'branch-merged' => '498214d',
- 'empty-branch' => '7efb185',
- 'ends-with.json' => '98b0d8b',
- 'flatten-dir' => 'e56497b',
- 'feature' => '0b4bc9a',
- 'feature_conflict' => 'bb5206f',
- 'fix' => '48f0be4',
- 'improve/awesome' => '5937ac0',
- 'merged-target' => '21751bf',
- 'markdown' => '0ed8c6c',
- 'lfs' => '55bc176',
- 'master' => 'b83d6e3',
- 'merge-test' => '5937ac0',
- "'test'" => 'e56497b',
- 'orphaned-branch' => '45127a9',
- 'binary-encoding' => '7b1cf43',
- 'gitattributes' => '5a62481',
- 'expand-collapse-diffs' => '4842455',
- 'symlink-expand-diff' => '81e6355',
- 'expand-collapse-files' => '025db92',
- 'expand-collapse-lines' => '238e82d',
- 'video' => '8879059',
- 'add-balsamiq-file' => 'b89b56d',
- 'crlf-diff' => '5938907',
- 'conflict-start' => '824be60',
- 'conflict-resolvable' => '1450cd6',
- 'conflict-binary-file' => '259a6fb',
- 'conflict-contains-conflict-markers' => '78a3086',
- 'conflict-missing-side' => 'eb227b3',
- 'conflict-non-utf8' => 'd0a293c',
- 'conflict-too-large' => '39fa04f',
- 'deleted-image-test' => '6c17798',
- 'wip' => 'b9238ee',
- 'csv' => '3dd0896',
- 'v1.1.0' => 'b83d6e3',
- 'add-ipython-files' => '93ee732',
- 'add-pdf-file' => 'e774ebd',
- 'add-pdf-text-binary' => '79faa7b',
- 'add_images_and_changes' => '010d106'
- }.freeze
-
- # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
- # need to keep all the branches in sync.
- # We currently only need a subset of the branches
- FORKED_BRANCH_SHA = {
- 'add-submodule-version-bump' => '3f547c0',
- 'master' => '5937ac0',
- 'remove-submodule' => '2a33e0c',
- 'conflict-resolvable-fork' => '404fa3f'
- }.freeze
-
- TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
- REPOS_STORAGE = 'default'.freeze
-
- # Test environment
- #
- # See gitlab.yml.example test section for paths
- #
- def init(opts = {})
- unless Rails.env.test?
- puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n"
- exit 1
- end
-
- # Disable mailer for spinach tests
- disable_mailer if opts[:mailer] == false
-
- clean_test_path
-
- # Setup GitLab shell for test instance
- setup_gitlab_shell
-
- setup_gitaly
-
- # Create repository for FactoryBot.create(:project)
- setup_factory_repo
-
- # Create repository for FactoryBot.create(:forked_project_with_submodules)
- setup_forked_repo
- end
-
- def disable_mailer
- allow_any_instance_of(NotificationService).to receive(:mailer)
- .and_return(double.as_null_object)
- end
-
- def enable_mailer
- allow_any_instance_of(NotificationService).to receive(:mailer)
- .and_call_original
- end
-
- def disable_pre_receive
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
- end
-
- # Clean /tmp/tests
- #
- # Keeps gitlab-shell and gitlab-test
- def clean_test_path
- Dir[TMP_TEST_PATH].each do |entry|
- unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/
- FileUtils.rm_rf(entry)
- end
- end
-
- FileUtils.mkdir_p(repos_path)
- FileUtils.mkdir_p(backup_path)
- FileUtils.mkdir_p(pages_path)
- FileUtils.mkdir_p(artifacts_path)
- end
-
- def clean_gitlab_test_path
- Dir[TMP_TEST_PATH].each do |entry|
- if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/
- FileUtils.rm_rf(entry)
- end
- end
- end
-
- def setup_gitlab_shell
- component_timed_setup('GitLab Shell',
- install_dir: Gitlab.config.gitlab_shell.path,
- version: Gitlab::Shell.version_required,
- task: 'gitlab:shell:install')
- end
-
- def setup_gitaly
- socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
- gitaly_dir = File.dirname(socket_path)
-
- component_timed_setup('Gitaly',
- install_dir: gitaly_dir,
- version: Gitlab::GitalyClient.expected_server_version,
- task: "gitlab:gitaly:install[#{gitaly_dir}]") do
-
- # Always re-create config, in case it's outdated. This is fast anyway.
- Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true)
-
- start_gitaly(gitaly_dir)
- end
- end
-
- def start_gitaly(gitaly_dir)
- if ENV['CI'].present?
- # Gitaly has been spawned outside this process already
- return
- end
-
- spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
- @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i }
- Kernel.at_exit { stop_gitaly }
-
- wait_gitaly
- end
-
- def wait_gitaly
- sleep_time = 10
- sleep_interval = 0.1
- socket = Gitlab::GitalyClient.address('default').sub('unix:', '')
-
- Integer(sleep_time / sleep_interval).times do
- begin
- Socket.unix(socket)
- return
- rescue
- sleep sleep_interval
- end
- end
-
- raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
- end
-
- def stop_gitaly
- return unless @gitaly_pid
-
- Process.kill('KILL', @gitaly_pid)
- rescue Errno::ESRCH
- # The process can already be gone if the test run was INTerrupted.
- end
-
- def setup_factory_repo
- setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
- BRANCH_SHA)
- end
-
- # This repo has a submodule commit that is not present in the main test
- # repository.
- def setup_forked_repo
- setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name,
- FORKED_BRANCH_SHA)
- end
-
- def setup_repo(repo_path, repo_path_bare, repo_name, refs)
- clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
-
- unless File.directory?(repo_path)
- system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
- end
-
- set_repo_refs(repo_path, refs)
-
- unless File.directory?(repo_path_bare)
- # We must copy bare repositories because we will push to them.
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
- end
- end
-
- def copy_repo(project, bare_repo:, refs:)
- target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.disk_path}.git")
- FileUtils.mkdir_p(target_repo_path)
- FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path)
- FileUtils.chmod_R 0755, target_repo_path
- set_repo_refs(target_repo_path, refs)
- end
-
- def repos_path
- Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
- end
-
- def backup_path
- Gitlab.config.backup.path
- end
-
- def pages_path
- Gitlab.config.pages.path
- end
-
- def artifacts_path
- Gitlab.config.artifacts.storage_path
- end
-
- # When no cached assets exist, manually hit the root path to create them
- #
- # Otherwise they'd be created by the first test, often timing out and
- # causing a transient test failure
- def eager_load_driver_server
- return unless defined?(Capybara)
-
- puts "Starting the Capybara driver server..."
- Capybara.current_session.visit '/'
- end
-
- def factory_repo_path_bare
- "#{factory_repo_path}_bare"
- end
-
- def forked_repo_path_bare
- "#{forked_repo_path}_bare"
- end
-
- def with_empty_bare_repository(name = nil)
- path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s
-
- yield(Rugged::Repository.init_at(path, :bare))
- ensure
- FileUtils.rm_rf(path)
- end
-
- private
-
- def factory_repo_path
- @factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name)
- end
-
- def factory_repo_name
- 'gitlab-test'
- end
-
- def forked_repo_path
- @forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name)
- end
-
- def forked_repo_name
- 'gitlab-test-fork'
- end
-
- # Prevent developer git configurations from being persisted to test
- # repositories
- def git_env
- { 'GIT_TEMPLATE_DIR' => '' }
- end
-
- def set_repo_refs(repo_path, branch_sha)
- instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
- update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
- reset = proc do
- Dir.chdir(repo_path) do
- IO.popen(update_refs, "w") { |io| io.write(instructions) }
- $?.success?
- end
- end
-
- # Try to reset without fetching to avoid using the network.
- unless reset.call
- raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
-
- # Before we used Git clone's --mirror option, bare repos could end up
- # with missing refs, clearing them and retrying should fix the issue.
- clean_gitlab_test_path && init unless reset.call
- end
- end
-
- def component_timed_setup(component, install_dir:, version:, task:)
- puts "\n==> Setting up #{component}..."
- start = Time.now
-
- ensure_component_dir_name_is_correct!(component, install_dir)
-
- # On CI, once installed, components never need update
- return if File.exist?(install_dir) && ENV['CI']
-
- if component_needs_update?(install_dir, version)
- # Cleanup the component entirely to ensure we start fresh
- FileUtils.rm_rf(install_dir)
-
- unless system('rake', task)
- raise ComponentFailedToInstallError
- end
- end
-
- yield if block_given?
-
- rescue ComponentFailedToInstallError
- puts "\n#{component} failed to install, cleaning up #{install_dir}!\n"
- FileUtils.rm_rf(install_dir)
- exit 1
- ensure
- puts " #{component} setup in #{Time.now - start} seconds...\n"
- end
-
- def ensure_component_dir_name_is_correct!(component, path)
- actual_component_dir_name = File.basename(path)
- expected_component_dir_name = component.parameterize
-
- unless actual_component_dir_name == expected_component_dir_name
- puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n"
- exit 1
- end
- end
-
- def component_needs_update?(component_folder, expected_version)
- # Allow local overrides of the component for tests during development
- return false if Rails.env.test? && File.symlink?(component_folder)
-
- version = File.read(File.join(component_folder, 'VERSION')).strip
-
- # Notice that this will always yield true when using branch versions
- # (`=branch_name`), but that actually makes sure the server is always based
- # on the latest branch revision.
- version != expected_version
- rescue Errno::ENOENT
- true
- end
-end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 0d24782f317..c9252bebb2e 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -125,6 +125,16 @@ describe 'gitlab:app namespace rake task' do
expect(Dir.entries(File.join(project.repository.path, 'custom_hooks'))).to include("dummy.txt")
end
end
+
+ context 'specific backup tasks' do
+ let(:task_list) { %w(db repo uploads builds artifacts pages lfs registry) }
+
+ it 'prints a progress message to stdout' do
+ task_list.each do |task|
+ expect { run_rake_task("gitlab:backup:#{task}:create") }.to output(/Dumping /).to_stdout
+ end
+ end
+ end
end
context 'tar creation' do
@@ -195,15 +205,12 @@ describe 'gitlab:app namespace rake task' do
end
context 'multiple repository storages' do
- let(:storage_default) do
- Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
- end
let(:test_second_storage) do
Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/custom_storage'))
end
let(:storages) do
{
- 'default' => storage_default,
+ 'default' => Gitlab.config.repositories.storages.default,
'test_second_storage' => test_second_storage
}
end
@@ -215,8 +222,7 @@ describe 'gitlab:app namespace rake task' do
before do
# We only need a backup of the repositories for this test
stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
- FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
- FileUtils.mkdir(Settings.absolute('tmp/tests/custom_storage'))
+
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
# Avoid asking gitaly about the root ref (which will fail beacuse of the
@@ -225,14 +231,23 @@ describe 'gitlab:app namespace rake task' do
end
after do
- FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
FileUtils.rm_rf(Settings.absolute('tmp/tests/custom_storage'))
end
it 'includes repositories in all repository storages' do
- project_a = create(:project, :repository, repository_storage: 'default')
+ project_a = create(:project, :repository)
project_b = create(:project, :repository, repository_storage: 'test_second_storage')
+ b_storage_dir = File.join(Settings.absolute('tmp/tests/custom_storage'), File.dirname(project_b.disk_path))
+
+ FileUtils.mkdir_p(b_storage_dir)
+
+ # Even when overriding the storage, we have to move it there, so it exists
+ FileUtils.mv(
+ File.join(Settings.absolute(storages['default'].legacy_disk_path), project_b.repository.disk_path + '.git'),
+ Rails.root.join(storages['test_second_storage'].legacy_disk_path, project_b.repository.disk_path + '.git')
+ )
+
expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout
tar_contents, exit_status = Gitlab::Popen.popen(
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index a2fb3886610..9f28510c3e4 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -46,8 +46,7 @@ describe LfsObjectUploader do
end
describe 'remote file' do
- let(:remote) { described_class::Store::REMOTE }
- let(:lfs_object) { create(:lfs_object, file_store: remote) }
+ let(:lfs_object) { create(:lfs_object, :object_storage, :with_file) }
context 'with object storage enabled' do
before do
@@ -57,16 +56,11 @@ describe LfsObjectUploader do
it 'can store file remotely' do
allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
- store_file(lfs_object)
+ lfs_object
- expect(lfs_object.file_store).to eq remote
+ expect(lfs_object.file_store).to eq(described_class::Store::REMOTE)
expect(lfs_object.file.path).not_to be_blank
end
end
end
-
- def store_file(lfs_object)
- lfs_object.file = fixture_file_upload(Rails.root.join("spec/fixtures/dk.png"), "`/png")
- lfs_object.save!
- end
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 16455e2517b..e7277b337f6 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -75,36 +75,8 @@ describe ObjectStorage do
expect(object).to receive(:file_store).and_return(nil)
end
- context 'when object storage is enabled' do
- context 'when direct uploads are enabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
- end
-
- it "uses Store::REMOTE" do
- is_expected.to eq(described_class::Store::REMOTE)
- end
- end
-
- context 'when direct uploads are disabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
- end
-
- it "uses Store::LOCAL" do
- is_expected.to eq(described_class::Store::LOCAL)
- end
- end
- end
-
- context 'when object storage is disabled' do
- before do
- stub_uploads_object_storage(uploader_class, enabled: false)
- end
-
- it "uses Store::LOCAL" do
- is_expected.to eq(described_class::Store::LOCAL)
- end
+ it "uses Store::LOCAL" do
+ is_expected.to eq(described_class::Store::LOCAL)
end
end
@@ -537,6 +509,72 @@ describe ObjectStorage do
end
end
+ context 'when local file is used' do
+ let(:temp_file) { Tempfile.new("test") }
+
+ before do
+ FileUtils.touch(temp_file)
+ end
+
+ after do
+ FileUtils.rm_f(temp_file)
+ end
+
+ context 'when valid file is used' do
+ context 'when valid file is specified' do
+ let(:uploaded_file) { temp_file }
+
+ context 'when object storage and direct upload is specified' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
+ end
+
+ context 'when file is stored' do
+ subject do
+ uploader.store!(uploaded_file)
+ end
+
+ it 'file to be remotely stored in permament location' do
+ subject
+
+ expect(uploader).to be_exists
+ expect(uploader).not_to be_cached
+ expect(uploader).not_to be_file_storage
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.path).not_to include('tmp/cache')
+ expect(uploader.object_store).to eq(described_class::Store::REMOTE)
+ end
+ end
+ end
+
+ context 'when object storage and direct upload is not used' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
+ end
+
+ context 'when file is stored' do
+ subject do
+ uploader.store!(uploaded_file)
+ end
+
+ it 'file to be remotely stored in permament location' do
+ subject
+
+ expect(uploader).to be_exists
+ expect(uploader).not_to be_cached
+ expect(uploader).to be_file_storage
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.path).not_to include('tmp/cache')
+ expect(uploader.object_store).to eq(described_class::Store::LOCAL)
+ end
+ end
+ end
+ end
+ end
+ end
+
context 'when remote file is used' do
let(:temp_file) { Tempfile.new("test") }
@@ -590,9 +628,9 @@ describe ObjectStorage do
expect(uploader).to be_exists
expect(uploader).to be_cached
+ expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
expect(uploader.path).not_to include('tmp/cache')
- expect(uploader.url).not_to be_nil
expect(uploader.path).not_to include('tmp/cache')
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end
@@ -607,6 +645,7 @@ describe ObjectStorage do
expect(uploader).to be_exists
expect(uploader).not_to be_cached
+ expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
expect(uploader.path).not_to include('tmp/upload')
expect(uploader.path).not_to include('tmp/cache')
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
index 7a7dcb71680..aed62f97448 100644
--- a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -7,113 +7,138 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
end
end
- let!(:projects) { create_list(:project, 10, :with_avatar) }
- let(:uploads) { Upload.all }
let(:model_class) { Project }
- let(:mounted_as) { :avatar }
+ let(:uploads) { Upload.all }
let(:to_store) { ObjectStorage::Store::REMOTE }
- before do
- stub_uploads_object_storage(AvatarUploader)
- end
-
- describe '.enqueue!' do
- def enqueue!
- described_class.enqueue!(uploads, Project, mounted_as, to_store)
- end
+ shared_examples "uploads migration worker" do
+ describe '.enqueue!' do
+ def enqueue!
+ described_class.enqueue!(uploads, Project, mounted_as, to_store)
+ end
- it 'is guarded by .sanity_check!' do
- expect(described_class).to receive(:perform_async)
- expect(described_class).to receive(:sanity_check!)
+ it 'is guarded by .sanity_check!' do
+ expect(described_class).to receive(:perform_async)
+ expect(described_class).to receive(:sanity_check!)
- enqueue!
- end
+ enqueue!
+ end
- context 'sanity_check! fails' do
- include_context 'sanity_check! fails'
+ context 'sanity_check! fails' do
+ include_context 'sanity_check! fails'
- it 'does not enqueue a job' do
- expect(described_class).not_to receive(:perform_async)
+ it 'does not enqueue a job' do
+ expect(described_class).not_to receive(:perform_async)
- expect { enqueue! }.to raise_error(described_class::SanityCheckError)
+ expect { enqueue! }.to raise_error(described_class::SanityCheckError)
+ end
end
end
- end
- describe '.sanity_check!' do
- shared_examples 'raises a SanityCheckError' do
- let(:mount_point) { nil }
+ describe '.sanity_check!' do
+ shared_examples 'raises a SanityCheckError' do
+ let(:mount_point) { nil }
- it do
- expect { described_class.sanity_check!(uploads, model_class, mount_point) }
- .to raise_error(described_class::SanityCheckError)
+ it do
+ expect { described_class.sanity_check!(uploads, model_class, mount_point) }
+ .to raise_error(described_class::SanityCheckError)
+ end
end
- end
- context 'uploader types mismatch' do
- let!(:outlier) { create(:upload, uploader: 'FileUploader') }
+ before do
+ stub_const("WrongModel", Class.new)
+ end
- include_examples 'raises a SanityCheckError'
- end
+ context 'uploader types mismatch' do
+ let!(:outlier) { create(:upload, uploader: 'GitlabUploader') }
- context 'model types mismatch' do
- let!(:outlier) { create(:upload, model_type: 'Potato') }
+ include_examples 'raises a SanityCheckError'
+ end
- include_examples 'raises a SanityCheckError'
- end
+ context 'model types mismatch' do
+ let!(:outlier) { create(:upload, model_type: 'WrongModel') }
- context 'mount point not found' do
- include_examples 'raises a SanityCheckError' do
- let(:mount_point) { :potato }
+ include_examples 'raises a SanityCheckError'
end
- end
- end
- describe '#perform' do
- def perform
- described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
- rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
- # swallow
+ context 'mount point not found' do
+ include_examples 'raises a SanityCheckError' do
+ let(:mount_point) { :potato }
+ end
+ end
end
- shared_examples 'outputs correctly' do |success: 0, failures: 0|
- total = success + failures
+ describe '#perform' do
+ def perform
+ described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
+ rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
+ # swallow
+ end
+
+ shared_examples 'outputs correctly' do |success: 0, failures: 0|
+ total = success + failures
- if success > 0
- it 'outputs the reports' do
- expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
+ if success > 0
+ it 'outputs the reports' do
+ expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
- perform
+ perform
+ end
end
- end
- if failures > 0
- it 'outputs upload failures' do
- expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
+ if failures > 0
+ it 'outputs upload failures' do
+ expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
- perform
+ perform
+ end
end
end
- end
- it_behaves_like 'outputs correctly', success: 10
+ it_behaves_like 'outputs correctly', success: 10
+
+ it 'migrates files' do
+ perform
- it 'migrates files' do
- perform
+ expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0)
+ end
- aggregate_failures do
- projects.each do |project|
- expect(project.reload.avatar.upload.local?).to be_falsey
+ context 'migration is unsuccessful' do
+ before do
+ allow_any_instance_of(ObjectStorage::Concern)
+ .to receive(:migrate!).and_raise(CarrierWave::UploadError, "I am a teapot.")
end
+
+ it_behaves_like 'outputs correctly', failures: 10
end
end
+ end
- context 'migration is unsuccessful' do
- before do
- allow_any_instance_of(ObjectStorage::Concern).to receive(:migrate!).and_raise(CarrierWave::UploadError, "I am a teapot.")
- end
+ context "for AvatarUploader" do
+ let!(:projects) { create_list(:project, 10, :with_avatar) }
+ let(:mounted_as) { :avatar }
- it_behaves_like 'outputs correctly', failures: 10
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+ end
+
+ it_behaves_like "uploads migration worker"
+ end
+
+ context "for FileUploader" do
+ let!(:projects) { create_list(:project, 10) }
+ let(:secret) { SecureRandom.hex }
+ let(:mounted_as) { nil }
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+
+ projects.map do |project|
+ uploader = FileUploader.new(project)
+ uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+ end
end
+
+ it_behaves_like "uploads migration worker"
end
end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 099baacf019..0e8b7c82d3a 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -4,6 +4,11 @@ describe 'admin/dashboard/index.html.haml' do
include Devise::Test::ControllerHelpers
before do
+ counts = Admin::DashboardController::COUNTED_ITEMS.each_with_object({}) do |item, hash|
+ hash[item] = 100
+ end
+
+ assign(:counts, counts)
assign(:projects, create_list(:project, 1))
assign(:users, create_list(:user, 1))
assign(:groups, create_list(:group, 1))
@@ -22,6 +27,6 @@ describe 'admin/dashboard/index.html.haml' do
it "includes revision of GitLab" do
render
- expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab::REVISION})"
+ expect(rendered).to have_content "#{Gitlab::VERSION} (#{Gitlab.revision})"
end
end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 0a78606171d..836d452304c 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -39,7 +39,7 @@ describe 'help/index' do
def stub_version(version, revision)
stub_const('Gitlab::VERSION', version)
- stub_const('Gitlab::REVISION', revision)
+ allow(Gitlab).to receive(:revision).and_return(revision)
end
def stub_helpers
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index f28bf430f02..98d4456b277 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -36,16 +36,17 @@ describe 'layouts/nav/sidebar/_project' do
expect(rendered).to have_text 'Registry'
end
- it 'highlights only one tab' do
+ it 'highlights sidebar item and flyout' do
render
- expect(rendered).to have_css('.active', count: 1)
+ expect(rendered).to have_css('.sidebar-top-level-items > li.active', count: 1)
+ expect(rendered).to have_css('.is-fly-out-only > li.active', count: 1)
end
- it 'highlights container registry tab only' do
+ it 'highlights container registry tab' do
render
- expect(rendered).to have_css('.active', text: 'Registry')
+ expect(rendered).to have_css('.sidebar-top-level-items > li.active', text: 'Registry')
end
end
end
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
index ec435ec3b32..32d73d0c5ab 100644
--- a/spec/views/projects/imports/new.html.haml_spec.rb
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -4,9 +4,10 @@ describe "projects/imports/new.html.haml" do
let(:user) { create(:user) }
context 'when import fails' do
- let(:project) { create(:project_empty_repo, import_status: :failed, import_error: '<a href="http://googl.com">Foo</a>', import_type: :gitlab_project, import_source: '/var/opt/gitlab/gitlab-rails/shared/tmp/project_exports/uploads/t.tar.gz', import_url: nil) }
+ let(:project) { create(:project_empty_repo, :import_failed, import_type: :gitlab_project, import_source: '/var/opt/gitlab/gitlab-rails/shared/tmp/project_exports/uploads/t.tar.gz', import_url: nil) }
before do
+ project.import_state.update_attributes(last_error: '<a href="http://googl.com">Foo</a>')
sign_in(user)
project.add_master(user)
end
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
new file mode 100644
index 00000000000..d15391911c1
--- /dev/null
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe 'projects/settings/ci_cd/_autodevops_form' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign :project, project
+ end
+
+ context 'when kubernetes is not active' do
+ context 'when auto devops domain is not defined' do
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a')
+ expect(rendered).to have_link('Kubernetes cluster')
+ end
+ end
+
+ context 'when auto devops domain is defined' do
+ before do
+ project.build_auto_devops(domain: 'example.com')
+ end
+
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a')
+ expect(rendered).to have_link('Kubernetes cluster')
+ end
+ end
+ end
+
+ context 'when kubernetes is active' do
+ before do
+ create(:kubernetes_service, project: project)
+ end
+
+ context 'when auto devops domain is not defined' do
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
+ end
+ end
+
+ context 'when auto devops domain is defined' do
+ before do
+ project.build_auto_devops(domain: 'example.com')
+ end
+
+ it 'does not show warning message' do
+ render
+
+ expect(rendered).not_to have_css('.settings-message')
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb
deleted file mode 100644
index be9a4d9c57c..00000000000
--- a/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require 'spec_helper'
-
-describe 'projects/settings/ci_cd/_form' do
- let(:project) { create(:project, :repository) }
-
- before do
- assign :project, project
- end
-
- context 'when kubernetes is not active' do
- context 'when auto devops domain is not defined' do
- it 'shows warning message' do
- render
-
- expect(rendered).to have_css('.settings-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a')
- expect(rendered).to have_link('Kubernetes cluster')
- end
- end
-
- context 'when auto devops domain is defined' do
- before do
- project.build_auto_devops(domain: 'example.com')
- end
-
- it 'shows warning message' do
- render
-
- expect(rendered).to have_css('.settings-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a')
- expect(rendered).to have_link('Kubernetes cluster')
- end
- end
- end
-
- context 'when kubernetes is active' do
- before do
- create(:kubernetes_service, project: project)
- end
-
- context 'when auto devops domain is not defined' do
- it 'shows warning message' do
- render
-
- expect(rendered).to have_css('.settings-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
- end
- end
-
- context 'when auto devops domain is defined' do
- before do
- project.build_auto_devops(domain: 'example.com')
- end
-
- it 'does not show warning message' do
- render
-
- expect(rendered).not_to have_css('.settings-message')
- end
- end
- end
-end
diff --git a/spec/workers/admin_email_worker_spec.rb b/spec/workers/admin_email_worker_spec.rb
new file mode 100644
index 00000000000..27687f069ea
--- /dev/null
+++ b/spec/workers/admin_email_worker_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe AdminEmailWorker do
+ subject(:worker) { described_class.new }
+
+ describe '.perform' do
+ it 'does not attempt to send repository check mail when they are disabled' do
+ stub_application_setting(repository_checks_enabled: false)
+
+ expect(worker).not_to receive(:send_repository_check_mail)
+
+ worker.perform
+ end
+
+ context 'repository_checks enabled' do
+ before do
+ stub_application_setting(repository_checks_enabled: true)
+ end
+
+ it 'checks if repository check mail should be sent' do
+ expect(worker).to receive(:send_repository_check_mail)
+
+ worker.perform
+ end
+
+ it 'does not send mail when there are no failed repos' do
+ expect(RepositoryCheckMailer).not_to receive(:notify)
+
+ worker.perform
+ end
+
+ it 'send mail when there is a failed repo' do
+ create(:project, last_repository_check_failed: true, last_repository_check_at: Date.yesterday)
+
+ expect(RepositoryCheckMailer).to receive(:notify).and_return(spy)
+
+ worker.perform
+ end
+ end
+ end
+end
diff --git a/spec/workers/create_note_diff_file_worker_spec.rb b/spec/workers/create_note_diff_file_worker_spec.rb
new file mode 100644
index 00000000000..0ac946a1232
--- /dev/null
+++ b/spec/workers/create_note_diff_file_worker_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe CreateNoteDiffFileWorker do
+ describe '#perform' do
+ let(:diff_note) { create(:diff_note_on_merge_request) }
+
+ it 'creates diff file' do
+ diff_note.note_diff_file.destroy!
+
+ expect_any_instance_of(DiffNote).to receive(:create_diff_file)
+ .and_call_original
+
+ described_class.new.perform(diff_note.id)
+ end
+ end
+end
diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
index 3be49a0dee8..0f78c5cc644 100644
--- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state do
- let(:project) { create(:project, import_jid: '123') }
+ let(:project) { create(:project) }
+ let(:import_state) { create(:import_state, project: project, jid: '123') }
let(:worker) { described_class.new }
describe '#perform' do
@@ -105,7 +106,8 @@ describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_st
# This test is there to make sure we only select the columns we care
# about.
- expect(found.attributes).to eq({ 'id' => nil, 'import_jid' => '123' })
+ # TODO: enable this assertion back again
+ # expect(found.attributes).to include({ 'id' => nil, 'import_jid' => '123' })
end
it 'returns nil if the project import is not running' do
diff --git a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
index 073c6d7a2f5..25ada575a44 100644
--- a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
@@ -14,7 +14,8 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do
end
describe '#perform' do
- let(:project) { create(:project, import_jid: '123abc') }
+ let(:project) { create(:project) }
+ let(:import_state) { create(:import_state, project: project, jid: '123abc') }
context 'when the project does not exist' do
it 'does nothing' do
@@ -70,20 +71,21 @@ describe Gitlab::GithubImport::RefreshImportJidWorker do
describe '#find_project' do
it 'returns a Project' do
- project = create(:project, import_status: 'started')
+ project = create(:project, :import_started)
expect(worker.find_project(project.id)).to be_an_instance_of(Project)
end
- it 'only selects the import JID field' do
- project = create(:project, import_status: 'started', import_jid: '123abc')
-
- expect(worker.find_project(project.id).attributes)
- .to eq({ 'id' => nil, 'import_jid' => '123abc' })
- end
+ # it 'only selects the import JID field' do
+ # project = create(:project, :import_started)
+ # project.import_state.update_attributes(jid: '123abc')
+ #
+ # expect(worker.find_project(project.id).attributes)
+ # .to eq({ 'id' => nil, 'import_jid' => '123abc' })
+ # end
it 'returns nil for a project for which the import process failed' do
- project = create(:project, import_status: 'failed')
+ project = create(:project, :import_failed)
expect(worker.find_project(project.id)).to be_nil
end
diff --git a/spec/workers/issue_due_scheduler_worker_spec.rb b/spec/workers/issue_due_scheduler_worker_spec.rb
index 7b60835fd26..2710267d384 100644
--- a/spec/workers/issue_due_scheduler_worker_spec.rb
+++ b/spec/workers/issue_due_scheduler_worker_spec.rb
@@ -14,7 +14,9 @@ describe IssueDueSchedulerWorker do
create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow)
create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today)
- expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async).with([[project1.id], [project2.id]])
+ expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async) do |args|
+ expect(args).to match_array([[project1.id], [project2.id]])
+ end
described_class.new.perform
end
diff --git a/spec/workers/mail_scheduler/issue_due_worker_spec.rb b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
index 48ac1b8a1a4..1026ae5b4bf 100644
--- a/spec/workers/mail_scheduler/issue_due_worker_spec.rb
+++ b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
@@ -12,8 +12,8 @@ describe MailScheduler::IssueDueWorker do
create(:issue, :opened, project: project, due_date: 2.days.from_now) # due on another day
create(:issue, :opened, due_date: Date.tomorrow) # different project
- expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue1)
- expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue2)
+ expect(worker.notification_service).to receive(:issue_due).with(issue1)
+ expect(worker.notification_service).to receive(:issue_due).with(issue2)
worker.perform(project.id)
end
diff --git a/spec/workers/mail_scheduler/notification_service_worker_spec.rb b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
new file mode 100644
index 00000000000..f725c8763a0
--- /dev/null
+++ b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe MailScheduler::NotificationServiceWorker do
+ let(:worker) { described_class.new }
+ let(:method) { 'new_key' }
+ set(:key) { create(:personal_key) }
+
+ def serialize(*args)
+ ActiveJob::Arguments.serialize(args)
+ end
+
+ describe '#perform' do
+ it 'deserializes arguments from global IDs' do
+ expect(worker.notification_service).to receive(method).with(key)
+
+ worker.perform(method, *serialize(key))
+ end
+
+ context 'when the arguments cannot be deserialized' do
+ it 'does nothing' do
+ expect(worker.notification_service).not_to receive(method)
+
+ worker.perform(method, key.to_global_id.to_s.succ)
+ end
+ end
+
+ context 'when the method is not a public method' do
+ it 'raises NoMethodError' do
+ expect { worker.perform('notifiable?', *serialize(key)) }.to raise_error(NoMethodError)
+ end
+ end
+ end
+
+ describe '.perform_async' do
+ it 'serializes arguments as global IDs when scheduling' do
+ Sidekiq::Testing.fake! do
+ described_class.perform_async(method, key)
+
+ expect(described_class.jobs.count).to eq(1)
+ expect(described_class.jobs.first).to include('args' => [method, *serialize(key)])
+ end
+ end
+ end
+end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index 479d9396eca..eec110dfbfb 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -22,13 +22,11 @@ describe NamespacelessProjectDestroyWorker do
end
end
- # Only possible with schema 20180222043024 and lower.
- # Project#namespace_id has not null constraint since then
- context 'project has no namespace', :migration, schema: 20180222043024 do
- let!(:project) do
- project = build(:project, namespace_id: nil)
- project.save(validate: false)
- project
+ context 'project has no namespace' do
+ let!(:project) { create(:project) }
+
+ before do
+ allow_any_instance_of(Project).to receive(:namespace).and_return(nil)
end
context 'project not a fork of another project' do
@@ -61,8 +59,7 @@ describe NamespacelessProjectDestroyWorker do
let!(:parent_project) { create(:project) }
let(:project) do
namespaceless_project = fork_project(parent_project)
- namespaceless_project.namespace_id = nil
- namespaceless_project.save(validate: false)
+ namespaceless_project.save
namespaceless_project
end
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 850b8cd8f5c..6cd27d2fafb 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -31,8 +31,8 @@ describe RepositoryCheck::BatchWorker do
it 'does nothing when repository checks are disabled' do
create(:project, created_at: 1.week.ago)
- current_settings = double('settings', repository_checks_enabled: false)
- expect(subject).to receive(:current_settings) { current_settings }
+
+ stub_application_setting(repository_checks_enabled: false)
expect(subject.perform).to eq(nil)
end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 1d9bbf2ca62..a021235aed6 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -2,44 +2,60 @@ require 'spec_helper'
require 'fileutils'
describe RepositoryCheck::SingleRepositoryWorker do
- subject { described_class.new }
+ subject(:worker) { described_class.new }
- it 'passes when the project has no push events' do
- project = create(:project_empty_repo, :wiki_disabled)
+ it 'skips when the project has no push events' do
+ project = create(:project, :repository, :wiki_disabled)
project.events.destroy_all
- break_repo(project)
+ break_project(project)
- subject.perform(project.id)
+ expect(worker).not_to receive(:git_fsck)
+
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(false)
end
it 'fails when the project has push events and a broken repository' do
- project = create(:project_empty_repo)
+ project = create(:project, :repository)
create_push_event(project)
- break_repo(project)
+ break_project(project)
- subject.perform(project.id)
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(true)
end
+ it 'succeeds when the project repo is valid' do
+ project = create(:project, :repository, :wiki_disabled)
+ create_push_event(project)
+
+ expect(worker).to receive(:git_fsck).and_call_original
+
+ expect do
+ worker.perform(project.id)
+ end.to change { project.reload.last_repository_check_at }
+
+ expect(project.reload.last_repository_check_failed).to eq(false)
+ end
+
it 'fails if the wiki repository is broken' do
- project = create(:project_empty_repo, :wiki_enabled)
+ project = create(:project, :repository, :wiki_enabled)
project.create_wiki
+ create_push_event(project)
# Test sanity: everything should be fine before the wiki repo is broken
- subject.perform(project.id)
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(false)
break_wiki(project)
- subject.perform(project.id)
+ worker.perform(project.id)
expect(project.reload.last_repository_check_failed).to eq(true)
end
it 'skips wikis when disabled' do
- project = create(:project_empty_repo, :wiki_disabled)
+ project = create(:project, :wiki_disabled)
# Make sure the test would fail if the wiki repo was checked
break_wiki(project)
@@ -49,8 +65,8 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'creates missing wikis' do
- project = create(:project_empty_repo, :wiki_enabled)
- FileUtils.rm_rf(wiki_path(project))
+ project = create(:project, :wiki_enabled)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
subject.perform(project.id)
@@ -58,34 +74,39 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'does not create a wiki if the main repo does not exist at all' do
- project = create(:project_empty_repo)
- create_push_event(project)
- FileUtils.rm_rf(project.repository.path_to_repo)
- FileUtils.rm_rf(wiki_path(project))
+ project = create(:project, :repository)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.path)
+ Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
subject.perform(project.id)
- expect(File.exist?(wiki_path(project))).to eq(false)
+ expect(Gitlab::Shell.new.exists?(project.repository_storage, project.wiki.path)).to eq(false)
end
- def break_wiki(project)
- objects_dir = wiki_path(project) + '/objects'
+ def create_push_event(project)
+ project.events.create(action: Event::PUSHED, author_id: create(:user).id)
+ end
- # Replace the /objects directory with a file so that the repo is
- # invalid, _and_ 'git init' cannot fix it.
- FileUtils.rm_rf(objects_dir)
- FileUtils.touch(objects_dir) if File.directory?(wiki_path(project))
+ def break_wiki(project)
+ break_repo(wiki_path(project))
end
def wiki_path(project)
project.wiki.repository.path_to_repo
end
- def create_push_event(project)
- project.events.create(action: Event::PUSHED, author_id: create(:user).id)
+ def break_project(project)
+ break_repo(project.repository.path_to_repo)
end
- def break_repo(project)
- FileUtils.rm_rf(File.join(project.repository.path_to_repo, 'objects'))
+ def break_repo(repo)
+ # Create or replace blob ffffffffffffffffffffffffffffffffffffffff with an empty file
+ # This will make the repo invalid, _and_ 'git init' cannot fix it.
+ path = File.join(repo, 'objects', 'ff')
+ file = File.join(path, 'ffffffffffffffffffffffffffffffffffffff')
+
+ FileUtils.mkdir_p(path)
+ FileUtils.rm_f(file)
+ FileUtils.touch(file)
end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 2b1a617ee62..84d1b38ef19 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -11,10 +11,12 @@ describe RepositoryImportWorker do
let(:project) { create(:project, :import_scheduled) }
context 'when worker was reset without cleanup' do
- let(:jid) { '12345678' }
- let(:started_project) { create(:project, :import_started, import_jid: jid) }
-
it 'imports the project successfully' do
+ jid = '12345678'
+ started_project = create(:project)
+
+ create(:import_state, :started, project: started_project, jid: jid)
+
allow(subject).to receive(:jid).and_return(jid)
expect_any_instance_of(Projects::ImportService).to receive(:execute)
diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb
new file mode 100644
index 00000000000..f22d7c1d073
--- /dev/null
+++ b/spec/workers/repository_remove_remote_worker_spec.rb
@@ -0,0 +1,50 @@
+require 'rails_helper'
+
+describe RepositoryRemoveRemoteWorker do
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ let(:remote_name) { 'joe'}
+ let!(:project) { create(:project, :repository) }
+
+ context 'when it cannot obtain lease' do
+ it 'logs error' do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
+
+ expect_any_instance_of(Repository).not_to receive(:remove_remote)
+ expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+
+ worker.perform(project.id, remote_name)
+ end
+ end
+
+ context 'when it gets the lease' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
+ end
+
+ context 'when project does not exist' do
+ it 'returns nil' do
+ expect(worker.perform(-1, 'remote_name')).to be_nil
+ end
+ end
+
+ context 'when project exists' do
+ it 'removes remote from repository' do
+ masterrev = project.repository.find_branch('master').dereferenced_target
+
+ create_remote_branch(remote_name, 'remote_branch', masterrev)
+
+ expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original
+
+ worker.perform(project.id, remote_name)
+ end
+ end
+ end
+ end
+
+ def create_remote_branch(remote_name, branch_name, target)
+ rugged = project.repository.rugged
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
+ end
+end
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
new file mode 100644
index 00000000000..152ba2509b9
--- /dev/null
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -0,0 +1,84 @@
+require 'rails_helper'
+
+describe RepositoryUpdateRemoteMirrorWorker do
+ subject { described_class.new }
+
+ let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
+ let(:scheduled_time) { Time.now - 5.minutes }
+
+ around do |example|
+ Timecop.freeze(Time.now) { example.run }
+ end
+
+ describe '#perform' do
+ context 'with status none' do
+ before do
+ remote_mirror.update_attributes(update_status: 'none')
+ end
+
+ it 'sets status as finished when update remote mirror service executes successfully' do
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
+
+ expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished')
+ end
+
+ it 'sets status as failed when update remote mirror service executes with errors' do
+ error_message = 'fail!'
+
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message)
+ expect do
+ subject.perform(remote_mirror.id, Time.now)
+ end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message)
+
+ expect(remote_mirror.reload.update_status).to eq('failed')
+ end
+
+ it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do
+ remote_mirror.update_attributes(last_update_started_at: Time.now)
+
+ expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true)
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror)
+
+ expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil
+ end
+ end
+
+ context 'with unexpected error' do
+ it 'marks mirror as failed' do
+ allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError)
+
+ expect do
+ subject.perform(remote_mirror.id, Time.now)
+ end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError)
+ expect(remote_mirror.reload.update_status).to eq('failed')
+ end
+ end
+
+ context 'with another worker already running' do
+ before do
+ remote_mirror.update_attributes(update_status: 'started')
+ end
+
+ it 'raises RemoteMirrorUpdateAlreadyInProgressError' do
+ expect do
+ subject.perform(remote_mirror.id, Time.now)
+ end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError)
+ end
+ end
+
+ context 'with status failed' do
+ before do
+ remote_mirror.update_attributes(update_status: 'failed')
+ end
+
+ it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do
+ remote_mirror.update_attributes(last_update_started_at: Time.now)
+
+ expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false)
+ expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
+
+ expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished')
+ 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 069514552b1..af7675c8cab 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -48,13 +48,21 @@ describe StuckImportJobsWorker do
describe 'with scheduled import_status' do
it_behaves_like 'project import job detection' do
- let(:project) { create(:project, :import_scheduled, import_jid: '123') }
+ let(:project) { create(:project, :import_scheduled) }
+
+ before do
+ project.import_state.update_attributes(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, import_jid: '123') }
+ let(:project) { create(:project, :import_started) }
+
+ before do
+ project.import_state.update_attributes(jid: '123')
+ end
end
end
end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 0fa19ac84bb..80137815d2b 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -18,8 +18,13 @@ describe UpdateMergeRequestsWorker do
end
it 'executes MergeRequests::RefreshService with expected values' do
- expect(MergeRequests::RefreshService).to receive(:new).with(project, user).and_call_original
- expect_any_instance_of(MergeRequests::RefreshService).to receive(:execute).with(oldrev, newrev, ref)
+ expect(MergeRequests::RefreshService).to receive(:new)
+ .with(project, user).and_wrap_original do |method, *args|
+ method.call(*args).tap do |refresh_service|
+ expect(refresh_service)
+ .to receive(:execute).with(oldrev, newrev, ref)
+ end
+ end
perform
end
diff --git a/vendor/assets/javascripts/peek.performance_bar.js b/vendor/assets/javascripts/peek.performance_bar.js
deleted file mode 100644
index 6ed86dce2f2..00000000000
--- a/vendor/assets/javascripts/peek.performance_bar.js
+++ /dev/null
@@ -1,182 +0,0 @@
-var PerformanceBar, ajaxStart, renderPerformanceBar, updateStatus;
-
-PerformanceBar = (function() {
- PerformanceBar.prototype.appInfo = null;
-
- PerformanceBar.prototype.width = null;
-
- PerformanceBar.formatTime = function(value) {
- if (value >= 1000) {
- return ((value / 1000).toFixed(3)) + "s";
- } else {
- return (value.toFixed(0)) + "ms";
- }
- };
-
- function PerformanceBar(options) {
- var k, v;
- if (options == null) {
- options = {};
- }
- this.el = $('#peek-view-performance-bar .performance-bar');
- for (k in options) {
- v = options[k];
- this[k] = v;
- }
- if (this.width == null) {
- this.width = this.el.width();
- }
- if (this.timing == null) {
- this.timing = window.performance.timing;
- }
- }
-
- PerformanceBar.prototype.render = function(serverTime) {
- var networkTime, perfNetworkTime;
- if (serverTime == null) {
- serverTime = 0;
- }
- this.el.empty();
- this.addBar('frontend', '#90d35b', 'domLoading', 'domInteractive');
- perfNetworkTime = this.timing.responseEnd - this.timing.requestStart;
- if (serverTime && serverTime <= perfNetworkTime) {
- networkTime = perfNetworkTime - serverTime;
- this.addBar('latency / receiving', '#f1faff', this.timing.requestStart + serverTime, this.timing.requestStart + serverTime + networkTime);
- this.addBar('app', '#90afcf', this.timing.requestStart, this.timing.requestStart + serverTime, this.appInfo);
- } else {
- this.addBar('backend', '#c1d7ee', 'requestStart', 'responseEnd');
- }
- this.addBar('tcp / ssl', '#45688e', 'connectStart', 'connectEnd');
- this.addBar('redirect', '#0c365e', 'redirectStart', 'redirectEnd');
- this.addBar('dns', '#082541', 'domainLookupStart', 'domainLookupEnd');
- return this.el;
- };
-
- PerformanceBar.prototype.isLoaded = function() {
- return this.timing.domInteractive;
- };
-
- PerformanceBar.prototype.start = function() {
- return this.timing.navigationStart;
- };
-
- PerformanceBar.prototype.end = function() {
- return this.timing.domInteractive;
- };
-
- PerformanceBar.prototype.total = function() {
- return this.end() - this.start();
- };
-
- PerformanceBar.prototype.addBar = function(name, color, start, end, info) {
- var bar, left, offset, time, title, width;
- if (typeof start === 'string') {
- start = this.timing[start];
- }
- if (typeof end === 'string') {
- end = this.timing[end];
- }
- if (!((start != null) && (end != null))) {
- return;
- }
- time = end - start;
- offset = start - this.start();
- left = this.mapH(offset);
- width = this.mapH(time);
- title = name + ": " + (PerformanceBar.formatTime(time));
- bar = $('<li></li>', {
- 'data-title': title,
- 'data-toggle': 'tooltip',
- 'data-container': 'body'
- });
- bar.css({
- width: width + "px",
- left: left + "px",
- background: color
- });
- return this.el.append(bar);
- };
-
- PerformanceBar.prototype.mapH = function(offset) {
- return offset * (this.width / this.total());
- };
-
- return PerformanceBar;
-
-})();
-
-renderPerformanceBar = function() {
- var bar, resp, span, time;
- resp = $('#peek-server_response_time');
- time = Math.round(resp.data('time') * 1000);
- bar = new PerformanceBar;
- bar.render(time);
- span = $('<span>', {
- 'data-toggle': 'tooltip',
- 'data-title': 'Total navigation time for this page.',
- 'data-container': 'body'
- }).text(PerformanceBar.formatTime(bar.total()));
- return updateStatus(span);
-};
-
-updateStatus = function(html) {
- return $('#serverstats').html(html);
-};
-
-ajaxStart = null;
-
-$(document).on('pjax:start page:fetch turbolinks:request-start', function(event) {
- return ajaxStart = event.timeStamp;
-});
-
-$(document).on('pjax:end page:load turbolinks:load', function(event, xhr) {
- var ajaxEnd, serverTime, total;
- if (ajaxStart == null) {
- return;
- }
- ajaxEnd = event.timeStamp;
- total = ajaxEnd - ajaxStart;
- serverTime = xhr ? parseInt(xhr.getResponseHeader('X-Runtime')) : 0;
- return setTimeout(function() {
- var bar, now, span, tech;
- now = new Date().getTime();
- bar = new PerformanceBar({
- timing: {
- requestStart: ajaxStart,
- responseEnd: ajaxEnd,
- domLoading: ajaxEnd,
- domInteractive: now
- },
- isLoaded: function() {
- return true;
- },
- start: function() {
- return ajaxStart;
- },
- end: function() {
- return now;
- }
- });
- bar.render(serverTime);
- if ($.fn.pjax != null) {
- tech = 'PJAX';
- } else {
- tech = 'Turbolinks';
- }
- span = $('<span>', {
- 'data-toggle': 'tooltip',
- 'data-title': tech + " navigation time",
- 'data-container': 'body'
- }).text(PerformanceBar.formatTime(total));
- updateStatus(span);
- return ajaxStart = null;
- }, 0);
-});
-
-$(function() {
- if (window.performance) {
- return renderPerformanceBar();
- } else {
- return $('#peek-view-performance-bar').remove();
- }
-});
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index a83a428c844..b09cb3dbc04 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,7 +4,8 @@
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
-.idea/dictionaries
+.idea/**/dictionaries
+.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore
index 6d21783d471..19cfe22f583 100644
--- a/vendor/gitignore/Global/Vim.gitignore
+++ b/vendor/gitignore/Global/Vim.gitignore
@@ -12,3 +12,5 @@ Session.vim
*~
# Auto-generated tag files
tags
+# Persistent undo
+[._]*.un~
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index 6143e53f9e3..a1c2a238a96 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -13,6 +13,7 @@
# Package Files #
*.jar
*.war
+*.nar
*.ear
*.zip
*.tar.gz
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 09dfede4814..86de6aa3f5f 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -52,7 +52,7 @@ Carthage/Build
fastlane/report.xml
fastlane/Preview.html
-fastlane/screenshots
+fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 161179bff3e..312d1f652c6 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -64,5 +64,5 @@ Carthage/Build
fastlane/report.xml
fastlane/Preview.html
-fastlane/screenshots
+fastlane/screenshots/**/*.png
fastlane/test_output
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index c560658e45c..e6598ba1727 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -198,6 +198,9 @@ pythontex-files-*/
# easy-todo
*.lod
+# xmpincl
+*.xmpi
+
# xindy
*.xdy
@@ -234,3 +237,6 @@ TSWLatexianTemp*
# standalone packages
*.sta
+
+# generated if using elsarticle.cls
+*.spl
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index a7c0c70a0b4..0210746b1a5 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -6,7 +6,7 @@
Assets/AssetStoreTools*
# Visual Studio cache directory
-/.vs/
+.vs/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
@@ -22,6 +22,7 @@ ExportedObj/
*.booproj
*.svd
*.pdb
+*.opendb
# Unity3D generated meta files
*.pidb.meta
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 29063cf6072..3e759b75bf4 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -240,6 +240,7 @@ Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
+*.rptproj.bak
# SQL Server files
*.mdf
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 4f4ed80d101..589ebcf1414 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -12,8 +12,10 @@
# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
# level, or manually added below.
#
-# If you want to deploy to staging first, or enable canary deploys,
-# uncomment the relevant jobs in the pipeline below.
+# Continuous deployment to production is enabled by default.
+# If you want to deploy to staging first, or enable incremental rollouts,
+# set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables.
+# If you want to use canary deployments, uncomment the canary job.
#
# If Auto DevOps fails to detect the proper buildpack, or if you want to
# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the
@@ -75,7 +77,7 @@ test:
only:
- branches
-codequality:
+code_quality:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -84,9 +86,9 @@ codequality:
- docker:stable-dind
script:
- setup_docker
- - codeclimate
+ - code_quality
artifacts:
- paths: [codeclimate.json]
+ paths: [gl-code-quality-report.json]
performance:
stage: performance
@@ -133,7 +135,8 @@ dependency_scanning:
- dependency_scanning
artifacts:
paths: [gl-dependency-scanning-report.json]
-sast:container:
+
+container_scanning:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -142,9 +145,9 @@ sast:container:
- docker:stable-dind
script:
- setup_docker
- - sast_container
+ - container_scanning
artifacts:
- paths: [gl-sast-container-report.json]
+ paths: [gl-container-scanning-report.json]
dast:
stage: dast
@@ -214,10 +217,10 @@ stop_review:
# Staging deploys are disabled by default since
# continuous deployment to production is enabled by default
# If you prefer to automatically deploy to staging and
-# only manually promote to production, enable this job by removing the dot (.),
-# and uncomment the `when: manual` line in the `production` job.
+# only manually promote to production, enable this job by setting
+# STAGING_ENABLED.
-.staging:
+staging:
stage: staging
script:
- check_kube_domain
@@ -234,10 +237,11 @@ stop_review:
refs:
- master
kubernetes: active
+ variables:
+ - $STAGING_ENABLED
# Canaries are disabled by default, but if you want them,
-# and know what the downsides are, enable this job by removing the dot (.),
-# and uncomment the `when: manual` line in the `production` job.
+# and know what the downsides are, enable this job by removing the dot (.).
.canary:
stage: canary
@@ -258,12 +262,53 @@ stop_review:
- master
kubernetes: active
-# This job continuously deploys to production on every push to `master`.
-# To make this a manual process, either because you're enabling `staging`
-# or `canary` deploys, or you simply want more control over when you deploy
-# to production, uncomment the `when: manual` line in the `production` job.
+.production: &production_template
+ stage: production
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - deploy
+ - delete canary
+ - delete rollout
+ - persist_environment_url
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+ artifacts:
+ paths: [environment_url.txt]
production:
+ <<: *production_template
+ only:
+ refs:
+ - master
+ kubernetes: active
+ except:
+ variables:
+ - $STAGING_ENABLED
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+production_manual:
+ <<: *production_template
+ when: manual
+ allow_failure: false
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $STAGING_ENABLED
+ except:
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+# This job implements incremental rollout on for every push to `master`.
+
+.rollout: &rollout_template
stage: production
script:
- check_kube_domain
@@ -272,7 +317,8 @@ production:
- ensure_namespace
- install_tiller
- create_secret
- - deploy
+ - deploy rollout $ROLLOUT_PERCENTAGE
+ - scale stable $((100-ROLLOUT_PERCENTAGE))
- delete canary
- persist_environment_url
environment:
@@ -280,11 +326,53 @@ production:
url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
artifacts:
paths: [environment_url.txt]
-# when: manual
+
+rollout 10%:
+ <<: *rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 10
+ when: manual
only:
refs:
- master
kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+rollout 25%:
+ <<: *rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 25
+ when: manual
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+rollout 50%:
+ <<: *rollout_template
+ variables:
+ ROLLOUT_PERCENTAGE: 50
+ when: manual
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
+
+rollout 100%:
+ <<: *production_template
+ when: manual
+ allow_failure: false
+ only:
+ refs:
+ - master
+ kubernetes: active
+ variables:
+ - $INCREMENTAL_ROLLOUT_ENABLED
# ---------------------------------------------------------------------------
@@ -300,7 +388,7 @@ production:
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- function sast_container() {
+ function container_scanning() {
if [[ -n "$CI_REGISTRY_USER" ]]; then
echo "Logging to GitLab Container Registry with CI credentials..."
docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
@@ -308,18 +396,20 @@ production:
fi
docker run -d --name db arminc/clair-db:latest
- docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1
+ 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
- ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
+ 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
}
- function codeclimate() {
+ function code_quality() {
docker run --env SOURCE_CODE="$PWD" \
--volume "$PWD":/code \
--volume /var/run/docker.sock:/var/run/docker.sock \
@@ -361,30 +451,19 @@ production:
esac
}
- function deploy() {
- track="${1-stable}"
- name="$CI_ENVIRONMENT_SLUG"
-
- if [[ "$track" != "stable" ]]; then
- name="$name-$track"
- fi
-
- replicas="1"
- service_enabled="false"
- postgres_enabled="$POSTGRES_ENABLED"
- # canary uses stable db
- [[ "$track" == "canary" ]] && postgres_enabled="false"
+ function get_replicas() {
+ track="${1:-stable}"
+ percentage="${2:-100}"
env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' )
env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' )
- if [[ "$track" == "stable" ]]; then
+ if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then
# for stable track get number of replicas from `PRODUCTION_REPLICAS`
eval new_replicas=\$${env_slug}_REPLICAS
if [[ -z "$new_replicas" ]]; then
new_replicas=$REPLICAS
fi
- service_enabled="true"
else
# for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
@@ -392,10 +471,37 @@ production:
eval new_replicas=\${env_track}_REPLICAS
fi
fi
- if [[ -n "$new_replicas" ]]; then
- replicas="$new_replicas"
+
+ replicas="${new_replicas:-1}"
+ replicas="$(($replicas * $percentage / 100))"
+
+ # always return at least one replicas
+ if [[ $replicas -gt 0 ]]; then
+ echo "$replicas"
+ else
+ echo 1
+ fi
+ }
+
+ function deploy() {
+ track="${1-stable}"
+ percentage="${2:-100}"
+ name="$CI_ENVIRONMENT_SLUG"
+
+ replicas="1"
+ service_enabled="true"
+ postgres_enabled="$POSTGRES_ENABLED"
+
+ # if track is different than stable,
+ # re-use all attached resources
+ if [[ "$track" != "stable" ]]; then
+ name="$name-$track"
+ service_enabled="false"
+ postgres_enabled="false"
fi
+ replicas=$(get_replicas "$track" "$percentage")
+
if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then
secret_name='gitlab-registry'
else
@@ -425,6 +531,27 @@ production:
chart/
}
+ function scale() {
+ track="${1-stable}"
+ percentage="${2-100}"
+ name="$CI_ENVIRONMENT_SLUG"
+
+ if [[ "$track" != "stable" ]]; then
+ name="$name-$track"
+ fi
+
+ replicas=$(get_replicas "$track" "$percentage")
+
+ if [[ -n "$(helm ls -q "^$name$")" ]]; then
+ helm upgrade --reuse-values \
+ --wait \
+ --set replicaCount="$replicas" \
+ --namespace="$KUBE_NAMESPACE" \
+ "$name" \
+ chart/
+ fi
+ }
+
function install_dependencies() {
apk add -U openssl curl tar gzip bash ca-certificates git
wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub
@@ -546,8 +673,8 @@ production:
kubectl create secret -n "$KUBE_NAMESPACE" \
docker-registry gitlab-registry \
--docker-server="$CI_REGISTRY" \
- --docker-username="$CI_REGISTRY_USER" \
- --docker-password="$CI_REGISTRY_PASSWORD" \
+ --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \
+ --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \
--docker-email="$GITLAB_USER_EMAIL" \
-o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
}
diff --git a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
index ff7c87c29f0..4d5b6484d6e 100644
--- a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml
@@ -7,7 +7,7 @@
image: "chef/chefdk"
services:
- - docker:stable-dind
+ - docker:dind
variables:
DOCKER_HOST: "tcp://docker:2375"
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index 58d48d1284b..eeefadaa019 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -2,7 +2,7 @@
image: docker:latest
services:
- - docker:stable-dind
+ - docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
index ba2efbd03a0..d5ee7ed2c13 100644
--- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -24,7 +24,7 @@ variables:
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
-# To keep cache across branches add 'key: "$CI_JOB_REF_NAME"'
+# To keep cache across branches add 'key: "$CI_JOB_NAME"'
cache:
paths:
- .m2/repository
diff --git a/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
index c9c35906d1c..10d0b05d9f8 100644
--- a/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Swift.gitlab-ci.yml
@@ -1,5 +1,5 @@
# Lifted from: https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/
-# This file assumes an own GitLab CI runner, setup on an OS X system.
+# This file assumes an own GitLab CI runner, setup on an macOS system.
stages:
- build
- archive
@@ -8,11 +8,11 @@ build_project:
stage: build
script:
- xcodebuild clean -project ProjectName.xcodeproj -scheme SchemeName | xcpretty
- - xcodebuild test -project ProjectName.xcodeproj -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 6s,OS=9.2' | xcpretty -s
+ - xcodebuild test -project ProjectName.xcodeproj -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 8,OS=11.3' | xcpretty -s
tags:
- - ios_9-2
- - xcode_7-2
- - osx_10-11
+ - ios_11-3
+ - xcode_9-3
+ - macos_10-13
archive_project:
stage: archive
@@ -25,6 +25,6 @@ archive_project:
paths:
- build/ProjectName.ipa
tags:
- - ios_9-2
- - xcode_7-2
- - osx_10-11
+ - ios_11-3
+ - xcode_9-3
+ - macos_10-13
diff --git a/vendor/ingress/values.yaml b/vendor/ingress/values.yaml
index a6b499953bf..d0c1673cefc 100644
--- a/vendor/ingress/values.yaml
+++ b/vendor/ingress/values.yaml
@@ -7,3 +7,8 @@ controller:
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "10254"
+
+rbac:
+ create: false
+ createRole: false
+ createClusterRole: false
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index ca88f867fe5..07861631a07 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -4,22 +4,19 @@
@babel/template,7.0.0-beta.32,MIT
@babel/traverse,7.0.0-beta.32,MIT
@babel/types,7.0.0-beta.32,MIT
-@gitlab-org/gitlab-svgs,1.17.0,SEE LICENSE IN LICENSE
+@gitlab-org/gitlab-svgs,1.20.0,SEE LICENSE IN LICENSE
+@mrmlnc/readdir-enhanced,2.2.1,MIT
+@sindresorhus/is,0.7.0,MIT
@types/jquery,2.0.48,MIT
-JSONStream,1.3.2,MIT
RedCloth,4.3.2,MIT
abbrev,1.0.9,ISC
abbrev,1.1.1,ISC
-accepts,1.3.3,MIT
accepts,1.3.4,MIT
ace-rails-ap,4.1.2,MIT
acorn,3.3.0,MIT
-acorn,4.0.13,MIT
-acorn,5.1.1,MIT
acorn,5.4.1,MIT
-acorn-dynamic-import,2.0.2,MIT
+acorn-dynamic-import,3.0.0,MIT
acorn-jsx,3.0.1,MIT
-acorn-node,1.3.0,Apache 2.0
actionmailer,4.2.10,MIT
actionpack,4.2.10,MIT
actionview,4.2.10,MIT
@@ -28,14 +25,12 @@ activemodel,4.2.10,MIT
activerecord,4.2.10,MIT
activesupport,4.2.10,MIT
acts-as-taggable-on,5.0.0,MIT
-address,1.0.3,MIT
addressable,2.5.2,Apache 2.0
addressparser,1.0.1,MIT
aes_key_wrap,1.0.1,MIT
after,0.8.2,MIT
agent-base,2.1.1,MIT
ajv,4.11.8,MIT
-ajv,5.2.2,MIT
ajv,5.5.2,MIT
ajv,6.1.1,MIT
ajv-keywords,1.5.1,MIT
@@ -52,8 +47,10 @@ ansi-escapes,3.0.0,MIT
ansi-html,0.0.7,Apache 2.0
ansi-regex,2.1.1,MIT
ansi-regex,3.0.0,MIT
+ansi-styles,1.0.0,MIT
ansi-styles,2.2.1,MIT
-ansi-styles,3.2.0,MIT
+ansi-styles,3.2.1,MIT
+any-observable,0.2.0,MIT
anymatch,1.3.2,ISC
anymatch,2.0.0,ISC
append-transform,0.4.0,MIT
@@ -65,14 +62,12 @@ arr-diff,2.0.0,MIT
arr-diff,4.0.0,MIT
arr-flatten,1.1.0,MIT
arr-union,3.1.0,MIT
-array-filter,0.0.1,MIT
+array-differ,1.0.0,MIT
array-find,1.0.0,MIT
array-find-index,1.0.2,MIT
array-flatten,1.1.1,MIT
array-flatten,2.1.1,MIT
array-includes,3.0.3,MIT
-array-map,0.0.0,MIT
-array-reduce,0.0.0,MIT
array-slice,0.2.3,MIT
array-union,1.0.2,MIT
array-uniq,1.0.3,MIT
@@ -88,11 +83,11 @@ asn1.js,4.10.1,MIT
assert,1.4.1,MIT
assert-plus,0.2.0,MIT
assert-plus,1.0.0,MIT
-asset_sync,2.2.0,MIT
+asset_sync,2.4.0,MIT
assign-symbols,1.0.0,MIT
+ast-types,0.10.1,MIT
ast-types,0.11.1,MIT
-astw,2.2.0,MIT
-async,0.9.2,MIT
+ast-types,0.11.3,MIT
async,1.5.2,MIT
async,2.1.5,MIT
async,2.4.1,MIT
@@ -102,7 +97,7 @@ async-limiter,1.0.0,MIT
asynckit,0.4.0,MIT
atob,2.0.3,(MIT OR Apache-2.0)
atomic,1.1.99,Apache 2.0
-attr_encrypted,3.0.3,MIT
+attr_encrypted,3.1.0,MIT
attr_required,1.0.0,MIT
autoprefixer,6.7.7,MIT
autoprefixer-rails,6.2.3,MIT
@@ -115,7 +110,7 @@ axios,0.15.3,MIT
axios,0.17.1,MIT
axios-mock-adapter,1.10.0,MIT
babel-code-frame,6.26.0,MIT
-babel-core,6.26.0,MIT
+babel-core,6.26.3,MIT
babel-eslint,8.0.2,MIT
babel-generator,6.26.0,MIT
babel-helper-bindify-decorators,6.24.1,MIT
@@ -132,20 +127,25 @@ babel-helper-regex,6.26.0,MIT
babel-helper-remap-async-to-generator,6.24.1,MIT
babel-helper-replace-supers,6.24.1,MIT
babel-helpers,6.24.1,MIT
-babel-loader,7.1.2,MIT
+babel-loader,7.1.4,MIT
babel-messages,6.23.0,MIT
babel-plugin-check-es2015-constants,6.22.0,MIT
-babel-plugin-istanbul,4.1.5,New BSD
+babel-plugin-istanbul,4.1.6,New BSD
+babel-plugin-rewire,1.1.0,ISC
babel-plugin-syntax-async-functions,6.13.0,MIT
babel-plugin-syntax-async-generators,6.13.0,MIT
+babel-plugin-syntax-class-constructor-call,6.18.0,MIT
babel-plugin-syntax-class-properties,6.13.0,MIT
babel-plugin-syntax-decorators,6.13.0,MIT
babel-plugin-syntax-dynamic-import,6.18.0,MIT
babel-plugin-syntax-exponentiation-operator,6.13.0,MIT
+babel-plugin-syntax-export-extensions,6.13.0,MIT
+babel-plugin-syntax-flow,6.18.0,MIT
babel-plugin-syntax-object-rest-spread,6.13.0,MIT
babel-plugin-syntax-trailing-function-commas,6.22.0,MIT
babel-plugin-transform-async-generator-functions,6.24.1,MIT
babel-plugin-transform-async-to-generator,6.24.1,MIT
+babel-plugin-transform-class-constructor-call,6.24.1,MIT
babel-plugin-transform-class-properties,6.24.1,MIT
babel-plugin-transform-decorators,6.24.1,MIT
babel-plugin-transform-define,1.3.0,MIT
@@ -172,6 +172,8 @@ babel-plugin-transform-es2015-template-literals,6.22.0,MIT
babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT
babel-plugin-transform-es2015-unicode-regex,6.24.1,MIT
babel-plugin-transform-exponentiation-operator,6.24.1,MIT
+babel-plugin-transform-export-extensions,6.22.0,MIT
+babel-plugin-transform-flow-strip-types,6.22.0,MIT
babel-plugin-transform-object-rest-spread,6.23.0,MIT
babel-plugin-transform-regenerator,6.26.0,MIT
babel-plugin-transform-strict-mode,6.24.1,MIT
@@ -179,6 +181,7 @@ babel-preset-es2015,6.24.1,MIT
babel-preset-es2016,6.24.1,MIT
babel-preset-es2017,6.24.1,MIT
babel-preset-latest,6.24.1,MIT
+babel-preset-stage-1,6.24.1,MIT
babel-preset-stage-2,6.24.1,MIT
babel-preset-stage-3,6.24.1,MIT
babel-register,6.26.0,MIT
@@ -189,6 +192,7 @@ babel-types,6.26.0,MIT
babosa,1.0.2,MIT
babylon,6.18.0,MIT
babylon,7.0.0-beta.32,MIT
+babylon,7.0.0-beta.44,MIT
backo2,1.0.2,MIT
balanced-match,0.4.2,MIT
balanced-match,1.0.0,MIT
@@ -206,13 +210,13 @@ better-assert,1.0.2,MIT
bfj-node4,5.2.1,MIT
big.js,3.1.3,MIT
binary-extensions,1.11.0,MIT
+binaryextensions,2.1.1,MIT
bindata,2.4.3,ruby
bitsyntax,0.0.4,UNKNOWN
bl,1.1.2,MIT
blackst0ne-mermaid,7.1.0-fixed,MIT
blob,0.0.4,MIT*
block-stream,0.0.9,ISC
-bluebird,3.5.0,MIT
bluebird,3.5.1,MIT
bn.js,4.11.8,MIT
body-parser,1.18.2,MIT
@@ -224,25 +228,19 @@ bootstrap-sass,3.3.6,MIT
bootstrap_form,2.7.0,MIT
boxen,1.3.0,MIT
brace-expansion,1.1.11,MIT
-brace-expansion,1.1.8,MIT
braces,0.1.5,MIT
braces,1.8.5,MIT
braces,2.3.1,MIT
brorand,1.1.0,MIT
browser,2.2.0,MIT
-browser-pack,6.0.4,MIT
-browser-resolve,1.11.2,MIT
-browserify,14.5.0,MIT
browserify-aes,1.1.1,MIT
browserify-cipher,1.0.0,MIT
browserify-des,1.0.0,MIT
browserify-rsa,4.0.1,MIT
browserify-sign,4.0.4,ISC
browserify-zlib,0.1.4,MIT
-browserify-zlib,0.2.0,MIT
browserslist,1.7.7,MIT
buffer,4.9.1,MIT
-buffer,5.1.0,MIT
buffer-indexof,1.1.0,MIT
buffer-more-ints,0.0.2,MIT
buffer-xor,1.0.3,MIT
@@ -254,13 +252,13 @@ bytes,2.5.0,MIT
bytes,3.0.0,MIT
cacache,10.0.4,ISC
cache-base,1.0.1,MIT
-cached-path-relative,1.0.1,MIT
+cacheable-request,2.1.4,MIT
+call-me-maybe,1.0.1,MIT
caller-path,0.1.0,MIT
callsite,1.0.0,MIT*
callsites,0.2.0,MIT
camelcase,1.2.1,MIT
camelcase,2.1.1,MIT
-camelcase,3.0.0,MIT
camelcase,4.1.0,MIT
camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
@@ -271,16 +269,19 @@ caseless,0.11.0,Apache 2.0
caseless,0.12.0,Apache 2.0
cause,0.1,MIT
center-align,0.1.3,MIT
+chalk,0.4.0,MIT
chalk,1.1.3,MIT
-chalk,2.3.0,MIT
-chalk,2.3.1,MIT
+chalk,2.4.0,MIT
+chalk,2.4.1,MIT
chardet,0.4.2,MIT
+charenc,0.0.2,New BSD
charlock_holmes,0.7.6,MIT
chart.js,1.0.2,MIT
check-types,7.3.0,MIT
chokidar,1.7.0,MIT
chokidar,2.0.2,MIT
chownr,1.0.1,ISC
+chrome-trace-event,0.1.2,MIT
chronic,0.10.2,MIT
chronic_duration,0.10.6,MIT
chunky_png,1.3.5,MIT
@@ -294,11 +295,21 @@ classlist-polyfill,1.2.0,Unlicense
cli-boxes,1.0.0,MIT
cli-cursor,1.0.2,MIT
cli-cursor,2.1.0,MIT
+cli-spinners,0.1.2,MIT
+cli-table,0.3.1,MIT
+cli-truncate,0.2.1,MIT
cli-width,2.1.0,ISC
clipboard,1.7.1,MIT
cliui,2.1.0,ISC
-cliui,3.2.0,ISC
+cliui,4.0.0,ISC
clone,1.0.2,MIT
+clone,1.0.3,MIT
+clone,2.1.1,MIT
+clone-buffer,1.0.0,MIT
+clone-response,1.0.2,MIT
+clone-stats,0.0.1,MIT
+clone-stats,1.0.0,MIT
+cloneable-readable,1.0.0,MIT
co,3.0.6,MIT
co,4.6.0,MIT
coa,1.0.1,MIT
@@ -310,11 +321,11 @@ color-convert,1.9.1,MIT
color-name,1.1.2,MIT
color-string,0.3.0,MIT
colormin,1.1.2,MIT
+colors,1.0.3,MIT
colors,1.1.2,MIT
combine-lists,1.0.1,MIT
-combine-source-map,0.7.2,MIT
-combine-source-map,0.8.0,MIT
combined-stream,1.0.6,MIT
+commander,2.13.0,MIT
commander,2.15.1,MIT
commondir,1.0.1,MIT
commonmarker,0.17.8,MIT
@@ -323,9 +334,8 @@ component-emitter,1.2.1,MIT
component-inherit,0.0.3,MIT*
compressible,2.0.11,MIT
compression,1.7.0,MIT
-compression-webpack-plugin,1.1.7,MIT
+compression-webpack-plugin,1.1.11,MIT
concat-map,0.0.1,MIT
-concat-stream,1.5.2,MIT
concat-stream,1.6.0,MIT
concurrent-ruby-ext,1.0.5,MIT
configstore,3.1.1,Simplified BSD
@@ -339,21 +349,18 @@ constants-browserify,1.0.0,MIT
contains-path,0.1.0,MIT
content-disposition,0.5.2,MIT
content-type,1.0.4,MIT
-convert-source-map,1.1.3,MIT
-convert-source-map,1.5.0,MIT
+convert-source-map,1.5.1,MIT
cookie,0.3.1,MIT
cookie-signature,1.0.6,MIT
copy-concurrently,1.0.5,ISC
copy-descriptor,0.1.1,MIT
-copy-webpack-plugin,4.4.1,MIT
+copy-webpack-plugin,4.5.1,MIT
core-js,2.3.0,MIT
-core-js,2.4.1,MIT
-core-js,2.5.1,MIT
core-js,2.5.3,MIT
core-util-is,1.0.2,MIT
-cosmiconfig,2.1.1,MIT
+cosmiconfig,2.2.2,MIT
crack,0.4.3,MIT
-crass,1.0.3,MIT
+crass,1.0.4,MIT
create-ecdh,4.0.0,MIT
create-error-class,3.0.2,MIT
create-hash,1.1.3,MIT
@@ -361,13 +368,14 @@ create-hmac,1.1.6,MIT
creole,0.5.0,ruby
cropper,2.3.0,MIT
cross-spawn,5.1.0,MIT
+cross-spawn,6.0.5,MIT
+crypt,0.0.2,New BSD
cryptiles,2.0.5,New BSD
cryptiles,3.1.2,New BSD
-crypto-browserify,3.11.0,MIT
crypto-browserify,3.12.0,MIT
crypto-random-string,1.0.0,MIT
css-color-names,0.0.4,MIT
-css-loader,0.28.9,MIT
+css-loader,0.28.11,MIT
css-selector-tokenizer,0.7.0,MIT
css_parser,1.5.0,MIT
cssesc,0.1.0,MIT
@@ -400,10 +408,13 @@ d3-transition,1.1.1,New BSD
d3_rails,3.5.11,MIT
dagre-d3-renderer,0.4.24,MIT
dagre-layout,0.8.0,MIT
+dargs,5.1.0,MIT
dashdash,1.14.1,MIT
data-uri-to-buffer,1.2.0,MIT
+date-fns,1.29.0,MIT
date-format,1.2.0,MIT
date-now,0.1.4,MIT
+dateformat,3.0.3,MIT
de-indent,1.0.2,MIT
debug,2.2.0,MIT
debug,2.6.8,MIT
@@ -418,6 +429,7 @@ decode-uri-component,0.2.0,MIT
decompress-response,3.3.0,MIT
deep-equal,1.0.1,MIT
deep-extend,0.4.2,MIT
+deep-extend,0.5.1,MIT
deep-is,0.1.3,MIT
default-require-extensions,1.0.0,MIT
default_value_for,3.0.2,MIT
@@ -433,19 +445,19 @@ delayed-stream,1.0.0,MIT
delegate,3.1.2,MIT
delegates,1.0.0,MIT
depd,1.1.1,MIT
-deps-sort,2.0.0,MIT
des.js,1.0.0,MIT
descendants_tracker,0.0.4,MIT
destroy,1.0.4,MIT
+detect-conflict,1.0.1,MIT
detect-indent,4.0.0,MIT
detect-libc,1.0.3,Apache 2.0
detect-node,2.0.3,ISC
-detect-port-alt,1.1.5,MIT
-detective,4.7.1,MIT
-devise,4.2.0,MIT
+device_detector,1.0.0,LGPL
+devise,4.4.3,MIT
devise-two-factor,3.0.0,MIT
di,0.0.1,MIT
diff,3.4.0,New BSD
+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
@@ -464,38 +476,43 @@ domelementtype,1.1.3,Simplified BSD
domelementtype,1.3.0,Simplified BSD
domhandler,2.4.1,Simplified BSD
domutils,1.6.2,Simplified BSD
-doorkeeper,4.3.1,MIT
+doorkeeper,4.3.2,MIT
doorkeeper-openid_connect,1.3.0,MIT
dot-prop,4.2.0,MIT
double-ended-queue,2.1.0-0,MIT
dropzone,4.2.0,MIT
dropzonejs-rails,0.7.2,MIT
duplexer,0.1.1,MIT
-duplexer2,0.1.4,New BSD
duplexer3,0.1.4,New BSD
duplexify,3.5.3,MIT
ecc-jsbn,0.1.1,MIT
+editions,1.3.4,MIT
ee-first,1.1.1,MIT
ejs,2.5.7,Apache 2.0
+ejs,2.5.9,Apache 2.0
electron-to-chromium,1.3.3,ISC
+elegant-spinner,1.0.1,MIT
elliptic,6.4.0,MIT
email_reply_trimmer,0.1.6,MIT
emoji-unicode-version,0.2.1,MIT
emojis-list,2.1.0,MIT
encodeurl,1.0.2,MIT
encryptor,3.0.0,MIT
-end-of-stream,1.4.0,MIT
end-of-stream,1.4.1,MIT
engine.io,3.1.5,MIT
engine.io-client,3.1.5,MIT
engine.io-parser,2.1.2,MIT
enhanced-resolve,0.9.1,MIT
-enhanced-resolve,3.4.1,MIT
+enhanced-resolve,4.0.0,MIT
ent,2.2.0,MIT
entities,1.1.1,Simplified BSD
+envinfo,4.4.2,MIT
equalizer,0.0.11,MIT
errno,0.1.4,MIT
+errno,0.1.7,MIT
+error,7.0.2,MIT
error-ex,1.3.0,MIT
+error-ex,1.3.1,MIT
erubis,2.7.0,MIT
es-abstract,1.10.0,MIT
es-to-primitive,1.1.1,MIT
@@ -525,7 +542,6 @@ eslint-plugin-promise,3.5.0,ISC
eslint-plugin-vue,4.0.1,MIT
eslint-scope,3.7.1,Simplified BSD
eslint-visitor-keys,1.0.0,Apache 2.0
-espree,3.5.0,Simplified BSD
espree,3.5.2,Simplified BSD
esprima,2.7.3,Simplified BSD
esprima,3.1.3,Simplified BSD
@@ -545,7 +561,7 @@ eventemitter3,1.2.0,MIT
events,1.1.1,MIT
eventsource,0.1.6,MIT
evp_bytestokey,1.0.3,MIT
-excon,0.60.0,MIT
+excon,0.62.0,MIT
execa,0.7.0,MIT
execjs,2.6.0,MIT
exit-hook,1.1.1,MIT
@@ -562,14 +578,16 @@ extend,3.0.1,MIT
extend-shallow,2.0.1,MIT
extend-shallow,3.0.2,MIT
external-editor,2.1.0,MIT
+external-editor,2.2.0,MIT
extglob,0.3.2,MIT
extglob,2.0.4,MIT
extsprintf,1.3.0,MIT
extsprintf,1.4.0,MIT
faraday,0.12.2,MIT
-faraday_middleware,0.11.0.1,MIT
+faraday_middleware,0.12.2,MIT
faraday_middleware-multi_json,0.0.6,MIT
fast-deep-equal,1.0.0,MIT
+fast-glob,2.2.1,MIT
fast-json-stable-stringify,2.0.0,MIT
fast-levenshtein,2.0.6,MIT
fast_blank,1.0.0,MIT
@@ -581,11 +599,10 @@ ffi,1.9.18,New BSD
figures,1.7.0,MIT
figures,2.0.0,MIT
file-entry-cache,2.0.0,MIT
-file-loader,1.1.8,MIT
+file-loader,1.1.11,MIT
file-uri-to-path,1.0.0,MIT
filename-regex,2.0.1,MIT
fileset,2.0.3,MIT
-filesize,3.5.11,New BSD
filesize,3.6.0,New BSD
fill-range,2.2.3,MIT
fill-range,4.0.0,MIT
@@ -594,11 +611,13 @@ find-cache-dir,1.0.0,MIT
find-root,0.1.2,MIT
find-up,1.1.2,MIT
find-up,2.1.0,MIT
+first-chunk-stream,2.0.0,MIT
flat-cache,1.2.2,MIT
flatten,1.0.2,MIT
flipper,0.13.0,MIT
flipper-active_record,0.13.0,MIT
flipper-active_support_cache_store,0.13.0,MIT
+flow-parser,0.66.0,MIT
flowdock,0.7.1,MIT
flush-write-stream,1.0.2,MIT
fog-aliyun,0.2.0,MIT
@@ -649,19 +668,25 @@ get_process_mem,0.2.0,MIT
getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.3.0,MIT
-gitaly-proto,0.94.0,MIT
+gh-got,6.0.0,MIT
+gitaly-proto,0.99.0,MIT
github-linguist,5.3.3,MIT
-github-markup,1.6.1,MIT
+github-markup,1.7.0,MIT
+github-username,4.1.0,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
+gitlab-gollum-lib,4.2.7.2,MIT
+gitlab-gollum-rugged_adapter,0.4.4,MIT
gitlab-grit,2.8.2,MIT
gitlab-markup,1.6.3,MIT
gitlab_omniauth-ldap,2.0.4,MIT
glob,5.0.15,ISC
glob,7.1.1,ISC
glob,7.1.2,ISC
+glob-all,3.1.0,MIT
glob-base,0.3.0,MIT
glob-parent,2.0.0,ISC
glob-parent,3.1.0,ISC
+glob-to-regexp,0.3.0,BSD
global-dirs,0.1.1,MIT
global-modules,1.0.0,MIT
global-prefix,1.0.2,MIT
@@ -671,10 +696,8 @@ globals,9.18.0,MIT
globby,5.0.0,MIT
globby,6.1.0,MIT
globby,7.1.1,MIT
-goldiloader,2.0.1,MIT
+globby,8.0.1,MIT
gollum-grit_adapter,1.0.1,MIT
-gollum-lib,4.2.7,MIT
-gollum-rugged_adapter,0.4.4,MIT
gon,6.1.0,MIT
good-listener,1.2.2,MIT
google-api-client,0.19.8,Apache 2.0
@@ -683,15 +706,16 @@ googleapis-common-protos-types,1.0.1,Apache 2.0
googleauth,0.6.2,Apache 2.0
got,6.7.1,MIT
got,7.1.0,MIT
+got,8.3.0,MIT
gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
grape,1.0.2,MIT
-grape-entity,0.6.0,MIT
+grape-entity,0.7.1,MIT
grape-route-helpers,2.1.0,MIT
grape_logging,1.7.0,MIT
graphlib,2.1.1,MIT
-grpc,1.10.0,Apache 2.0
-gzip-size,3.0.0,MIT
+grouped-queue,0.3.3,MIT
+grpc,1.11.0,Apache 2.0
gzip-size,4.1.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
@@ -704,6 +728,7 @@ har-validator,5.0.3,ISC
has,1.0.1,MIT
has-ansi,2.0.0,MIT
has-binary2,1.0.2,MIT
+has-color,0.1.7,MIT
has-cors,1.1.0,MIT
has-flag,1.0.0,MIT
has-flag,2.0.0,MIT
@@ -739,16 +764,16 @@ html-entities,1.2.0,MIT
html-pipeline,2.7.1,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
-htmlescape,1.1.1,MIT
htmlparser2,3.9.2,MIT
http,2.2.2,MIT
+http-cache-semantics,3.8.1,Simplified BSD
http-cookie,1.0.3,MIT
http-deceiver,1.2.7,MIT
http-errors,1.6.2,MIT
http-form_data,1.0.3,MIT
http-proxy,1.16.2,MIT
http-proxy-agent,1.0.0,MIT
-http-proxy-middleware,0.17.4,MIT
+http-proxy-middleware,0.18.0,MIT
http-signature,1.1.1,MIT
http-signature,1.2.0,MIT
http_parser.rb,0.6.0,MIT
@@ -757,7 +782,6 @@ httpclient,2.8.3,ruby
httpntlm,1.6.1,MIT
httpreq,0.4.24,MIT
https-browserify,0.0.1,MIT
-https-browserify,1.0.0,MIT
https-proxy-agent,1.0.0,MIT
i18n,0.9.5,MIT
ice_nine,0.11.2,MIT
@@ -767,7 +791,6 @@ icss-replace-symbols,1.1.0,ISC
icss-utils,2.1.0,ISC
ieee754,1.1.8,New BSD
iferr,0.1.5,MIT
-ignore,3.3.3,MIT
ignore,3.3.7,MIT
ignore-by-default,1.0.1,ISC
immediate,3.0.6,MIT
@@ -776,6 +799,7 @@ import-local,1.0.0,MIT
imports-loader,0.8.0,MIT
imurmurhash,0.1.4,MIT
indent-string,2.1.0,MIT
+indent-string,3.2.0,MIT
indexes-of,1.0.1,MIT
indexof,0.0.1,MIT*
inflection,1.10.0,MIT
@@ -785,12 +809,13 @@ influxdb,0.2.3,MIT
inherits,2.0.1,ISC
inherits,2.0.3,ISC
ini,1.3.5,ISC
-inline-source-map,0.6.2,MIT
inquirer,0.12.0,MIT
inquirer,3.3.0,MIT
-insert-module-globals,7.0.1,MIT
+inquirer,5.2.0,MIT
internal-ip,1.2.0,MIT
interpret,1.0.1,MIT
+interpret,1.1.0,MIT
+into-stream,3.1.0,MIT
invariant,2.2.2,New BSD
invert-kv,1.0.0,MIT
ip,1.0.1,MIT
@@ -803,7 +828,6 @@ is-accessor-descriptor,0.1.6,MIT
is-accessor-descriptor,1.0.0,MIT
is-arrayish,0.2.1,MIT
is-binary-path,1.0.1,MIT
-is-buffer,1.1.5,MIT
is-buffer,1.1.6,MIT
is-builtin-module,1.0.0,MIT
is-callable,1.1.3,MIT
@@ -812,6 +836,7 @@ is-data-descriptor,1.0.0,MIT
is-date-object,1.0.1,MIT
is-descriptor,0.1.6,MIT
is-descriptor,1.0.2,MIT
+is-directory,0.3.1,MIT
is-dotfile,1.0.3,MIT
is-equal-shallow,0.1.3,MIT
is-extendable,0.1.1,MIT
@@ -826,7 +851,6 @@ is-glob,3.1.0,MIT
is-glob,4.0.0,MIT
is-installed-globally,0.1.0,MIT
is-my-ip-valid,1.0.0,MIT
-is-my-json-valid,2.16.0,MIT
is-my-json-valid,2.17.2,MIT
is-npm,1.0.0,MIT
is-number,0.1.1,MIT
@@ -835,6 +859,7 @@ is-number,3.0.0,MIT
is-number,4.0.0,MIT
is-obj,1.0.1,MIT
is-object,1.0.1,MIT
+is-observable,0.2.0,MIT
is-odd,2.0.0,MIT
is-path-cwd,1.0.0,MIT
is-path-in-cwd,1.0.0,MIT
@@ -850,7 +875,7 @@ is-regex,1.0.4,MIT
is-relative,0.2.1,MIT
is-resolvable,1.0.0,MIT
is-retry-allowed,1.1.0,MIT
-is-root,1.0.0,MIT
+is-scoped,1.0.0,MIT
is-stream,1.1.0,MIT
is-svg,2.1.0,MIT
is-symbol,1.0.1,MIT
@@ -864,7 +889,6 @@ isarray,0.0.1,MIT
isarray,1.0.0,MIT
isarray,2.0.1,MIT
isbinaryfile,3.0.2,MIT
-isexe,1.1.2,ISC
isexe,2.0.0,ISC
isobject,2.1.0,MIT
isobject,3.0.1,MIT
@@ -872,11 +896,14 @@ isstream,0.1.2,MIT
istanbul,0.4.5,New BSD
istanbul-api,1.2.1,New BSD
istanbul-lib-coverage,1.1.1,New BSD
+istanbul-lib-coverage,1.2.0,New BSD
istanbul-lib-hook,1.1.0,New BSD
+istanbul-lib-instrument,1.10.1,New BSD
istanbul-lib-instrument,1.9.1,New BSD
istanbul-lib-report,1.1.2,New BSD
istanbul-lib-source-maps,1.2.2,New BSD
istanbul-reports,1.1.3,New BSD
+istextorbinary,2.2.1,MIT
isurl,1.0.0,MIT
jasmine-core,2.9.0,MIT
jasmine-jquery,2.1.1,MIT
@@ -889,23 +916,25 @@ jquery.waitforimages,2.2.0,MIT
js-base64,2.1.9,New BSD
js-cookie,2.1.3,MIT
js-tokens,3.0.2,MIT
+js-yaml,3.11.0,MIT
js-yaml,3.7.0,MIT
js-yaml,3.9.1,MIT
jsbn,0.1.1,MIT
+jscodeshift,0.4.1,New BSD
+jscodeshift,0.5.0,New BSD
jsesc,0.5.0,MIT
jsesc,1.3.0,MIT
json,1.8.6,ruby
+json-buffer,3.0.0,MIT
json-jwt,1.9.2,MIT
-json-loader,0.5.7,MIT
+json-parse-better-errors,1.0.2,MIT
json-schema,0.2.3,BSD
json-schema-traverse,0.3.1,MIT
-json-stable-stringify,0.0.1,MIT
json-stable-stringify,1.0.1,MIT
json-stringify-safe,5.0.1,ISC
json3,3.3.2,MIT
json5,0.5.1,MIT
jsonify,0.0.0,Public Domain
-jsonparse,1.3.1,MIT
jsonpointer,4.0.1,MIT
jsprim,1.4.1,MIT
jszip,3.1.3,(MIT OR GPL-3.0)
@@ -915,14 +944,15 @@ kaminari,1.0.1,MIT
kaminari-actionview,1.0.1,MIT
kaminari-activerecord,1.0.1,MIT
kaminari-core,1.0.1,MIT
-karma,2.0.0,MIT
+karma,2.0.2,MIT
karma-chrome-launcher,2.2.0,MIT
-karma-coverage-istanbul-reporter,1.4.1,MIT
+karma-coverage-istanbul-reporter,1.4.2,MIT
karma-jasmine,1.1.1,MIT
karma-mocha-reporter,2.2.5,MIT
karma-sourcemap-loader,0.3.7,MIT
-karma-webpack,2.0.7,MIT
+karma-webpack,3.0.0,MIT
katex,0.8.3,MIT
+keyv,3.0.0,MIT
kgio,2.10.0,LGPL-2.1+
killable,1.0.0,ISC
kind-of,3.2.2,MIT
@@ -930,27 +960,28 @@ kind-of,4.0.0,MIT
kind-of,5.1.0,MIT
kind-of,6.0.2,MIT
kubeclient,3.0.0,MIT
-labeled-stream-splicer,2.0.0,MIT
latest-version,3.1.0,MIT
lazy-cache,1.0.4,MIT
lazy-cache,2.0.2,MIT
lcid,1.0.0,MIT
levn,0.3.0,MIT
-lexical-scope,1.2.0,MIT
libbase64,0.1.0,MIT
libmime,3.0.0,MIT
libqp,1.1.0,MIT
licensee,8.9.2,MIT
lie,3.1.1,MIT
+listr,0.13.0,MIT
+listr-silent-renderer,1.1.1,MIT
+listr-update-renderer,0.4.0,MIT
+listr-verbose-renderer,0.4.1,MIT
little-plugger,1.1.4,MIT
load-json-file,1.1.0,MIT
-load-json-file,2.0.0,MIT
+load-json-file,4.0.0,MIT
loader-runner,2.3.0,MIT
-loader-utils,0.2.16,MIT
loader-utils,1.1.0,MIT
locale,2.1.2,"ruby,LGPLv3+"
locate-path,2.0.0,MIT
-lodash,3.10.1,MIT
+lodash,4.17.10,MIT
lodash,4.17.4,MIT
lodash,4.17.5,MIT
lodash._baseget,3.7.2,MIT
@@ -968,24 +999,28 @@ lodash.isarray,3.0.4,MIT
lodash.isfunction,3.0.9,MIT
lodash.isstring,4.0.1,MIT
lodash.kebabcase,4.0.1,MIT
-lodash.memoize,3.0.4,MIT
lodash.memoize,4.1.2,MIT
lodash.mergewith,4.6.0,MIT
lodash.snakecase,4.0.1,MIT
lodash.startswith,4.2.1,MIT
lodash.uniq,4.5.0,MIT
lodash.words,4.2.0,MIT
+log-symbols,1.0.2,MIT
log-symbols,2.1.0,MIT
+log-symbols,2.2.0,MIT
+log-update,1.0.2,MIT
log4js,2.5.3,Apache 2.0
logging,2.2.2,MIT
loggly,1.1.1,MIT
loglevel,1.4.1,MIT
-lograge,0.5.1,MIT
+loglevelnext,1.0.3,MIT
+lograge,0.10.0,MIT
longest,1.0.1,MIT
loofah,2.2.2,MIT
loose-envify,1.3.1,MIT
loud-rejection,1.6.0,MIT
lowercase-keys,1.0.0,MIT
+lru-cache,2.2.4,MIT
lru-cache,2.6.5,ISC
lru-cache,4.1.1,ISC
macaddress,0.2.8,MIT
@@ -994,6 +1029,7 @@ mail_room,0.9.1,MIT
mailcomposer,4.0.1,MIT
mailgun-js,0.7.15,MIT
make-dir,1.0.0,MIT
+make-dir,1.2.0,MIT
map-cache,0.2.2,MIT
map-obj,1.0.1,MIT
map-stream,0.1.0,UNKNOWN
@@ -1004,19 +1040,25 @@ math-expression-evaluator,1.2.16,MIT
md5.js,1.3.4,MIT
media-typer,0.3.0,MIT
mem,1.1.0,MIT
+mem-fs,1.1.3,MIT
+mem-fs-editor,4.0.1,MIT
memoist,0.16.0,MIT
memory-fs,0.2.0,MIT
memory-fs,0.4.1,MIT
meow,3.7.0,MIT
merge-descriptors,1.0.1,MIT
+merge2,1.2.2,MIT
method_source,0.8.2,MIT
methods,1.1.2,MIT
micromatch,2.3.11,MIT
+micromatch,3.1.10,MIT
micromatch,3.1.6,MIT
+micromatch,3.1.9,MIT
miller-rabin,4.0.1,MIT
mime,1.4.1,MIT
mime,1.6.0,MIT
-mime-db,1.29.0,MIT
+mime,2.2.0,MIT
+mime,2.3.1,MIT
mime-db,1.33.0,MIT
mime-types,2.1.18,MIT
mime-types,3.1,MIT
@@ -1028,15 +1070,14 @@ mini_mime,1.0.0,MIT
mini_portile2,2.3.0,MIT
minimalistic-assert,1.0.0,ISC
minimalistic-crypto-utils,1.0.1,MIT
-minimatch,3.0.3,ISC
minimatch,3.0.4,ISC
minimist,0.0.10,MIT
minimist,0.0.8,MIT
+minimist,0.1.0,MIT
minimist,1.2.0,MIT
mississippi,2.0.0,Simplified BSD
mixin-deep,1.3.1,MIT
mkdirp,0.5.1,MIT
-module-deps,4.1.1,MIT
moment,2.19.2,MIT
monaco-editor,0.10.0,MIT
mousetrap,1.4.6,Apache 2.0
@@ -1048,21 +1089,24 @@ multi_json,1.13.1,MIT
multi_xml,0.6.0,MIT
multicast-dns,6.1.1,MIT
multicast-dns-service-types,1.1.0,MIT
+multimatch,2.1.0,MIT
multipart-post,2.0.0,MIT
mustermann,1.0.2,MIT
mustermann-grape,1.0.0,MIT
mute-stream,0.0.5,ISC
mute-stream,0.0.7,ISC
mysql2,0.4.10,MIT
-name-all-modules-plugin,1.0.1,MIT
nan,2.8.0,MIT
nanomatch,1.2.9,MIT
natural-compare,1.4.0,MIT
negotiator,0.6.1,MIT
+neo-async,2.5.0,MIT
net-ldap,0.16.0,MIT
net-ssh,4.2.0,MIT
netmask,1.0.6,MIT
netrc,0.11.0,MIT
+nice-try,1.0.4,MIT
+node-dir,0.1.8,MIT
node-forge,0.6.33,New BSD
node-libs-browser,1.1.1,MIT
node-libs-browser,2.0.0,MIT
@@ -1075,8 +1119,9 @@ nodemailer-shared,1.1.0,MIT
nodemailer-smtp-pool,2.8.2,MIT
nodemailer-smtp-transport,2.7.2,MIT
nodemailer-wellknown,0.1.10,MIT
-nodemon,1.15.1,MIT
+nodemon,1.17.3,MIT
nokogiri,1.8.2,MIT
+nomnom,1.8.1,MIT
nopt,1.0.10,MIT
nopt,3.0.6,ISC
nopt,4.0.1,ISC
@@ -1084,6 +1129,7 @@ normalize-package-data,2.4.0,Simplified BSD
normalize-path,2.1.1,MIT
normalize-range,0.1.2,MIT
normalize-url,1.9.1,MIT
+normalize-url,2.0.1,MIT
npm-run-path,2.0.2,MIT
npmlog,4.1.2,ISC
null-check,1.0.0,MIT
@@ -1108,10 +1154,9 @@ omniauth-authentiq,0.3.1,MIT
omniauth-azure-oauth2,0.0.9,MIT
omniauth-cas3,1.1.4,MIT
omniauth-facebook,4.0.0,MIT
-omniauth-github,1.1.2,MIT
+omniauth-github,1.3.0,MIT
omniauth-gitlab,1.0.2,MIT
omniauth-google-oauth2,0.5.3,MIT
-omniauth-jwt,0.0.2,MIT
omniauth-kerberos,0.3.0,MIT
omniauth-multipassword,0.4.2,MIT
omniauth-oauth,1.1.0,MIT
@@ -1130,36 +1175,39 @@ opener,1.4.3,(WTFPL OR MIT)
opn,5.2.0,MIT
optimist,0.6.1,MIT
optionator,0.8.2,MIT
+ora,0.2.3,MIT
org-ruby,0.9.12,MIT
original,1.0.0,MIT
orm_adapter,0.5.0,MIT
os,0.9.6,MIT
os-browserify,0.2.1,MIT
-os-browserify,0.3.0,MIT
os-homedir,1.0.2,MIT
-os-locale,1.4.0,MIT
os-locale,2.1.0,MIT
os-tmpdir,1.0.2,MIT
osenv,0.1.5,ISC
p-cancelable,0.3.0,MIT
+p-cancelable,0.4.1,MIT
+p-each-series,1.0.0,MIT
p-finally,1.0.0,MIT
-p-limit,1.1.0,MIT
+p-is-promise,1.1.0,MIT
+p-lazy,1.0.0,MIT
p-limit,1.2.0,MIT
p-locate,2.0.0,MIT
p-map,1.1.1,MIT
-p-timeout,1.2.0,MIT
+p-reduce,1.0.0,MIT
+p-timeout,1.2.1,MIT
+p-timeout,2.0.1,MIT
p-try,1.0.0,MIT
pac-proxy-agent,1.1.0,MIT
pac-resolver,2.0.0,MIT
package-json,4.0.1,MIT
pako,0.2.9,MIT
-pako,1.0.5,(MIT AND Zlib)
pako,1.0.6,(MIT AND Zlib)
parallel-transform,1.1.0,MIT
-parents,1.0.1,MIT
parse-asn1,5.1.0,ISC
parse-glob,3.0.4,MIT
parse-json,2.2.0,MIT
+parse-json,4.0.0,MIT
parse-passwd,1.0.0,MIT
parseqs,0.0.5,MIT
parseuri,0.0.5,MIT
@@ -1173,18 +1221,15 @@ path-is-absolute,1.0.1,MIT
path-is-inside,1.0.2,(WTFPL OR MIT)
path-key,2.0.1,MIT
path-parse,1.0.5,MIT
-path-platform,0.11.15,MIT
path-proxy,1.0.0,MIT
path-to-regexp,0.1.7,MIT
path-type,1.1.0,MIT
-path-type,2.0.0,MIT
path-type,3.0.0,MIT
pause-stream,0.0.11,Apache 2.0
pbkdf2,3.0.14,MIT
peek,1.0.1,MIT
peek-gc,0.0.2,MIT
peek-mysql2,1.1.0,MIT
-peek-performance_bar,1.3.1,MIT
peek-pg,1.3.0,MIT
peek-rblineprof,0.2.0,MIT
peek-redis,1.2.0,MIT
@@ -1206,9 +1251,8 @@ portfinder,1.0.13,MIT
posix-character-classes,0.1.1,MIT
posix-spawn,0.3.13,MIT
postcss,5.2.16,MIT
-postcss,6.0.14,MIT
-postcss,6.0.15,MIT
postcss,6.0.19,MIT
+postcss,6.0.21,MIT
postcss-calc,5.3.1,MIT
postcss-colormin,2.2.2,MIT
postcss-convert-values,2.6.1,MIT
@@ -1248,13 +1292,15 @@ prelude-ls,1.1.2,MIT
premailer,1.10.4,New BSD
premailer-rails,1.9.7,MIT
prepend-http,1.0.4,MIT
+prepend-http,2.0.0,MIT
preserve,0.2.0,MIT
+prettier,1.10.2,MIT
prettier,1.11.1,MIT
prettier,1.8.2,MIT
+pretty-bytes,4.0.2,MIT
prismjs,1.6.0,MIT
private,0.1.8,MIT
process,0.11.10,MIT
-process,0.11.9,MIT
process-nextick-args,1.0.7,MIT
process-nextick-args,2.0.0,MIT
progress,1.1.8,MIT
@@ -1263,6 +1309,7 @@ promise-inflight,1.0.1,ISC
proxy-addr,2.0.3,MIT
proxy-agent,2.0.0,MIT
prr,0.0.0,MIT
+prr,1.0.1,MIT
ps-tree,1.1.0,MIT
pseudomap,1.0.2,ISC
pstree.remy,1.1.0,MIT
@@ -1280,11 +1327,12 @@ qs,6.2.3,New BSD
qs,6.4.0,New BSD
qs,6.5.1,New BSD
query-string,4.3.2,MIT
+query-string,5.1.1,MIT
querystring,0.2.0,MIT
querystring-es3,0.2.1,MIT
querystringify,0.0.4,MIT
querystringify,1.0.0,MIT
-rack,1.6.9,MIT
+rack,1.6.10,MIT
rack-accept,0.4.5,MIT
rack-attack,4.4.1,MIT
rack-cors,1.0.2,MIT
@@ -1300,7 +1348,7 @@ rails-i18n,4.0.9,MIT
railties,4.2.10,MIT
rainbow,2.2.2,MIT
raindrops,0.18.0,LGPL-2.1+
-rake,12.3.0,MIT
+rake,12.3.1,MIT
randomatic,1.1.7,MIT
randombytes,2.0.6,MIT
randomfill,1.0.4,MIT
@@ -1313,27 +1361,24 @@ rb-fsevent,0.10.2,MIT
rb-inotify,0.9.10,MIT
rbnacl,4.0.2,MIT
rbnacl-libsodium,1.0.11,MIT
-rc,1.2.1,(BSD-2-Clause OR MIT OR Apache-2.0)
rc,1.2.5,(BSD-2-Clause OR MIT OR Apache-2.0)
rdoc,4.2.2,ruby
re2,1.1.1,New BSD
-react-dev-utils,5.0.0,MIT
-react-error-overlay,4.0.0,MIT
-read-only-stream,2.0.0,MIT
+read-chunk,2.1.0,MIT
read-pkg,1.1.0,MIT
-read-pkg,2.0.0,MIT
+read-pkg,3.0.0,MIT
read-pkg-up,1.0.1,MIT
-read-pkg-up,2.0.0,MIT
+read-pkg-up,3.0.0,MIT
readable-stream,1.1.14,MIT
readable-stream,2.0.6,MIT
-readable-stream,2.3.3,MIT
readable-stream,2.3.4,MIT
readdirp,2.1.0,MIT
readline2,1.0.1,MIT
recaptcha,3.0.0,MIT
+recast,0.12.9,MIT
+recast,0.14.7,MIT
rechoir,0.6.2,MIT
recursive-open-struct,1.0.5,MIT
-recursive-readdir,2.2.1,MIT
redcarpet,3.4.0,MIT
redent,1.0.0,MIT
redis,2.8.0,MIT
@@ -1364,6 +1409,8 @@ repeat-element,1.1.2,MIT
repeat-string,0.2.2,MIT
repeat-string,1.6.1,MIT
repeating,2.0.1,MIT
+replace-ext,0.0.1,MIT
+replace-ext,1.0.0,MIT
representable,3.0.4,MIT
request,2.75.0,Apache 2.0
request,2.81.0,Apache 2.0
@@ -1378,24 +1425,27 @@ require-uncached,1.0.3,MIT
requires-port,1.0.0,MIT
resolve,1.1.7,MIT
resolve,1.5.0,MIT
+resolve,1.7.1,MIT
resolve-cwd,2.0.0,MIT
resolve-dir,1.0.1,MIT
resolve-from,1.0.1,MIT
resolve-from,3.0.0,MIT
resolve-url,0.2.1,MIT
-responders,2.3.0,MIT
+responders,2.4.0,MIT
+responselike,1.0.2,MIT
rest-client,2.0.2,MIT
restore-cursor,1.0.1,MIT
restore-cursor,2.0.0,MIT
ret,0.1.15,MIT
retriable,3.1.1,MIT
right-align,0.1.3,MIT
+rimraf,2.2.8,MIT
rimraf,2.6.1,ISC
rimraf,2.6.2,ISC
rinku,2.0.0,ISC
ripemd160,2.0.1,MIT
rotp,2.1.2,MIT
-rouge,2.2.1,MIT
+rouge,3.1.1,MIT
rqrcode,0.7.0,MIT
rqrcode-rails3,0.1.7,MIT
ruby-enum,0.7.2,MIT
@@ -1413,6 +1463,7 @@ run-queue,1.0.3,ISC
rx-lite,3.1.2,Apache 2.0
rx-lite,4.0.8,Apache 2.0
rx-lite-aggregates,4.0.8,Apache 2.0
+rxjs,5.5.10,Apache 2.0
safe-buffer,5.1.1,MIT
safe-regex,1.1.0,MIT
safe_yaml,1.0.4,MIT
@@ -1423,8 +1474,8 @@ sass-listen,4.0.0,MIT
sass-rails,5.0.6,MIT
sawyer,0.8.1,MIT
sax,1.2.2,ISC
-schema-utils,0.3.0,MIT
schema-utils,0.4.5,MIT
+scoped-regex,1.0.0,MIT
securecompare,1.0.0,MIT
seed-fu,2.3.7,MIT
select,1.1.2,MIT
@@ -1452,11 +1503,11 @@ setprototypeof,1.1.0,ISC
settingslogic,2.0.9,MIT
sexp_processor,4.9.0,MIT
sha.js,2.4.10,MIT
-shasum,1.0.2,MIT
+sha1,1.1.1,New BSD
shebang-command,1.2.0,MIT
shebang-regex,1.0.0,MIT
-shell-quote,1.6.1,MIT
shelljs,0.7.8,New BSD
+shelljs,0.8.1,New BSD
sidekiq,5.0.5,LGPL
sidekiq-cron,0.6.0,MIT
sidekiq-limit_fetch,3.4.0,MIT
@@ -1466,6 +1517,7 @@ slack-node,0.2.0,MIT
slack-notifier,1.5.1,MIT
slash,1.0.0,MIT
slice-ansi,0.0.4,MIT
+slide,1.1.6,ISC
smart-buffer,1.1.15,MIT
smtp-connection,2.12.0,MIT
snapdragon,0.8.1,MIT
@@ -1483,6 +1535,7 @@ socks,1.1.10,MIT
socks,1.1.9,MIT
socks-proxy-agent,2.1.1,MIT
sort-keys,1.1.2,MIT
+sort-keys,2.0.0,MIT
source-list-map,2.0.0,MIT
source-map,0.2.0,New BSD
source-map,0.4.4,New BSD
@@ -1516,50 +1569,51 @@ statuses,1.3.1,MIT
statuses,1.4.0,MIT
stream-browserify,2.0.1,MIT
stream-combiner,0.0.4,MIT
-stream-combiner2,1.1.1,MIT
stream-each,1.2.2,MIT
-stream-http,2.6.3,MIT
stream-http,2.8.0,MIT
stream-shift,1.0.0,MIT
-stream-splicer,2.0.0,MIT
+stream-to-observable,0.2.0,MIT
streamroller,0.7.0,MIT
strict-uri-encode,1.1.0,MIT
+string-template,0.2.1,MIT
string-width,1.0.2,MIT
-string-width,2.0.0,MIT
string-width,2.1.1,MIT
string_decoder,0.10.31,MIT
string_decoder,1.0.3,MIT
-stringex,2.7.1,MIT
+stringex,2.8.4,MIT
stringstream,0.0.5,MIT
+strip-ansi,0.1.1,MIT
strip-ansi,3.0.1,MIT
strip-ansi,4.0.0,MIT
strip-bom,2.0.0,MIT
strip-bom,3.0.0,MIT
+strip-bom-stream,2.0.0,MIT
strip-eof,1.0.0,MIT
strip-indent,1.0.1,MIT
strip-json-comments,2.0.1,MIT
-style-loader,0.20.2,MIT
-subarg,1.0.0,MIT
+style-loader,0.21.0,MIT
supports-color,2.0.0,MIT
supports-color,3.2.3,MIT
-supports-color,4.2.1,MIT
-supports-color,4.5.0,MIT
supports-color,5.1.0,MIT
supports-color,5.2.0,MIT
+supports-color,5.4.0,MIT
svg4everybody,2.1.9,CC0-1.0
svgo,0.7.2,MIT
-syntax-error,1.4.0,MIT
+symbol-observable,0.2.4,MIT
+symbol-observable,1.0.1,MIT
sys-filesystem,1.1.6,Artistic 2.0
table,3.8.3,New BSD
tapable,0.1.10,MIT
-tapable,0.2.8,MIT
+tapable,1.0.0,MIT
tar,2.2.1,ISC
tar-pack,3.4.1,Simplified BSD
+temp,0.8.3,MIT
temple,0.7.7,MIT
term-size,1.2.0,MIT
-test-exclude,4.1.1,ISC
+test-exclude,4.2.1,ISC
text,1.3.1,MIT
text-table,0.2.0,MIT
+textextensions,2.2.0,MIT
thor,0.19.4,MIT
thread_safe,0.3.6,Apache 2.0
three,0.84.0,MIT
@@ -1570,7 +1624,6 @@ through2,2.0.3,MIT
thunkify,2.1.2,MIT
thunky,0.1.0,MIT*
tilt,2.0.6,MIT
-time-stamp,2.0.0,MIT
timeago.js,3.0.2,MIT
timed-out,4.0.1,MIT
timers-browserify,1.4.2,MIT
@@ -1585,6 +1638,7 @@ to-fast-properties,1.0.3,MIT
to-fast-properties,2.0.0,MIT
to-object-path,0.3.0,MIT
to-regex,3.0.1,MIT
+to-regex,3.0.2,MIT
to-regex-range,2.1.1,MIT
toml-rb,1.0.0,MIT
touch,3.1.0,ISC
@@ -1597,7 +1651,6 @@ tryer,1.0.0,MIT
tryit,1.0.3,MIT
tsscmp,1.0.5,MIT
tty-browserify,0.0.0,MIT
-tty-browserify,0.0.1,MIT
tunnel-agent,0.4.3,Apache 2.0
tunnel-agent,0.6.0,Apache 2.0
tweetnacl,0.14.5,Unlicense
@@ -1608,16 +1661,17 @@ tzinfo,1.2.5,MIT
u2f,0.2.1,MIT
uber,0.1.0,MIT
uglifier,2.7.2,MIT
+uglify-es,3.3.9,Simplified BSD
uglify-js,2.8.29,Simplified BSD
uglify-to-browserify,1.0.2,MIT
-uglifyjs-webpack-plugin,0.4.6,MIT
+uglifyjs-webpack-plugin,1.2.5,MIT
uid-number,0.0.6,ISC
ultron,1.1.1,MIT
-umd,3.0.1,MIT
unc-path-regex,0.1.2,MIT
undefsafe,2.0.2,MIT
+underscore,1.6.0,MIT
underscore,1.7.0,MIT
-underscore,1.8.3,MIT
+underscore,1.9.0,MIT
unf,0.1.4,BSD
unf_ext,0.0.7.5,MIT
unicorn,5.1.0,ruby
@@ -1631,25 +1685,30 @@ unique-slug,2.0.0,ISC
unique-string,1.0.0,MIT
unpipe,1.0.0,MIT
unset-value,1.0.0,MIT
+untildify,3.0.2,MIT
unzip-response,2.0.1,MIT
upath,1.0.2,MIT
update-notifier,2.3.0,Simplified BSD
urix,0.1.0,MIT
url,0.11.0,MIT
-url-loader,0.6.2,MIT
+url-join,2.0.5,MIT
+url-join,4.0.0,MIT
+url-loader,1.0.1,MIT
url-parse,1.0.5,MIT
url-parse,1.1.9,MIT
url-parse-lax,1.0.0,MIT
+url-parse-lax,3.0.0,MIT
url-to-options,1.0.1,MIT
url_safe_base64,0.2.2,MIT
use,2.0.2,MIT
user-home,2.0.0,MIT
-useragent,2.3.0,MIT
+useragent,2.2.1,MIT
util,0.10.3,MIT
util-deprecate,1.0.2,MIT
utils-merge,1.0.1,MIT
uuid,3.2.1,MIT
uws,9.14.0,Zlib
+v8-compile-cache,1.1.2,MIT
validate-npm-package-license,3.0.1,Apache 2.0
validates_hostname,1.0.6,MIT
vary,1.1.1,MIT
@@ -1657,38 +1716,46 @@ vary,1.1.2,MIT
vendors,1.0.1,MIT
verror,1.10.0,MIT
version_sorter,2.1.0,MIT
+vinyl,1.2.0,MIT
+vinyl,2.1.0,MIT
+vinyl-file,2.0.0,MIT
virtus,1.0.5,MIT
visibilityjs,1.2.4,MIT
vm-browserify,0.0.4,MIT
vmstat,2.3.0,MIT
void-elements,2.0.1,MIT
-vue,2.5.13,MIT
+vue,2.5.16,MIT
vue-eslint-parser,2.0.1,MIT
-vue-hot-reload-api,2.2.4,MIT
-vue-loader,14.1.1,MIT
-vue-resource,1.3.5,MIT
+vue-hot-reload-api,2.3.0,MIT
+vue-loader,14.2.2,MIT
+vue-resource,1.5.0,MIT
vue-router,3.0.1,MIT
-vue-style-loader,4.0.2,MIT
-vue-template-compiler,2.5.13,MIT
+vue-style-loader,4.1.0,MIT
+vue-template-compiler,2.5.16,MIT
vue-template-es2015-compiler,1.6.0,MIT
+vue-virtual-scroll-list,1.2.5,MIT
vuex,3.0.1,MIT
-warden,1.2.6,MIT
-watchpack,1.4.0,MIT
+warden,1.2.7,MIT
+watchpack,1.5.0,MIT
wbuf,1.7.2,MIT
-webpack,3.11.0,MIT
-webpack-bundle-analyzer,2.10.0,MIT
-webpack-dev-middleware,1.12.2,MIT
-webpack-dev-server,2.11.2,MIT
+webpack,4.7.0,MIT
+webpack-addons,1.1.5,MIT
+webpack-bundle-analyzer,2.11.1,MIT
+webpack-cli,2.1.2,MIT
+webpack-dev-middleware,2.0.6,MIT
+webpack-dev-middleware,3.1.3,MIT
+webpack-dev-server,3.1.4,MIT
+webpack-log,1.1.2,MIT
+webpack-log,1.2.0,MIT
webpack-rails,0.9.10,MIT
webpack-sources,1.0.1,MIT
-webpack-stats-plugin,0.1.5,MIT
+webpack-sources,1.1.0,MIT
+webpack-stats-plugin,0.2.1,MIT
websocket-driver,0.6.5,MIT
websocket-extensions,0.1.1,MIT
when,3.7.8,MIT
whet.extend,0.9.9,MIT
-which,1.2.12,ISC
which,1.3.0,ISC
-which-module,1.0.0,ISC
which-module,2.0.0,ISC
wide-align,1.1.2,ISC
widest-line,2.0.0,MIT
@@ -1697,10 +1764,12 @@ window-size,0.1.0,MIT
wordwrap,0.0.2,MIT
wordwrap,0.0.3,MIT
wordwrap,1.0.0,MIT
-worker-loader,1.1.0,MIT
+worker-farm,1.5.2,MIT
+worker-loader,1.1.1,MIT
wrap-ansi,2.1.0,MIT
wrappy,1.0.2,ISC
write,0.2.1,MIT
+write-file-atomic,1.3.4,ISC
write-file-atomic,2.3.0,ISC
ws,3.3.3,MIT
ws,4.0.0,MIT
@@ -1712,9 +1781,12 @@ xtend,4.0.1,MIT
y18n,3.2.1,ISC
y18n,4.0.0,ISC
yallist,2.1.2,ISC
+yargs,1.2.6,MIT
+yargs,11.0.0,MIT
+yargs,11.1.0,MIT
yargs,3.10.0,MIT
-yargs,6.6.0,MIT
-yargs,8.0.2,MIT
-yargs-parser,4.2.1,ISC
-yargs-parser,7.0.0,ISC
+yargs-parser,9.0.2,ISC
yeast,0.1.2,MIT
+yeoman-environment,2.0.5,Simplified BSD
+yeoman-environment,2.0.6,Simplified BSD
+yeoman-generator,2.0.5,Simplified BSD
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index dcf5e4a0416..8dd5fa36987 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index d4856090ed9..89337dc5c31 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 6ee7e76f676..31c90d0820f 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index 7cf8f11d2c1..af8785bbc66 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,20 +54,38 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@1.23.0":
+"@gitlab-org/gitlab-svgs@^1.23.0":
version "1.23.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.23.0.tgz#42047aeedcc06bc12d417ed1efadad1749af9670"
+"@mrmlnc/readdir-enhanced@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+ dependencies:
+ call-me-maybe "^1.0.1"
+ glob-to-regexp "^0.3.0"
+
+"@sindresorhus/is@^0.7.0":
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
+
"@types/jquery@^2.0.40":
version "2.0.48"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef"
-JSONStream@^1.0.3:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
+"@vue/component-compiler-utils@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.2.1.tgz#3d543baa75cfe5dab96e29415b78366450156ef6"
dependencies:
- jsonparse "^1.2.0"
- through ">=2.2.7 <3"
+ consolidate "^0.15.1"
+ hash-sum "^1.0.2"
+ lru-cache "^4.1.2"
+ merge-source-map "^1.1.0"
+ postcss "^6.0.20"
+ postcss-selector-parser "^3.1.1"
+ prettier "^1.11.1"
+ source-map "^0.5.6"
+ vue-template-es2015-compiler "^1.6.0"
abbrev@1:
version "1.1.1"
@@ -77,25 +95,18 @@ abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
-accepts@~1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
- dependencies:
- mime-types "~2.1.11"
- negotiator "0.6.1"
-
-accepts@~1.3.4:
+accepts@~1.3.3, accepts@~1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
dependencies:
mime-types "~2.1.16"
negotiator "0.6.1"
-acorn-dynamic-import@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
+acorn-dynamic-import@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278"
dependencies:
- acorn "^4.0.3"
+ acorn "^5.0.0"
acorn-jsx@^3.0.0:
version "3.0.1"
@@ -103,33 +114,14 @@ acorn-jsx@^3.0.0:
dependencies:
acorn "^3.0.4"
-acorn-node@^1.2.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.3.0.tgz#5f86d73346743810ef1269b901dbcbded020861b"
- dependencies:
- acorn "^5.4.1"
- xtend "^4.0.1"
-
acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^4.0.3:
- version "4.0.13"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
-
-acorn@^5.0.0, acorn@^5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
-
-acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.1:
+acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102"
-address@1.0.3, address@^1.0.1:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
-
addressparser@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746"
@@ -160,15 +152,6 @@ ajv@^4.7.0, ajv@^4.9.1:
co "^4.6.0"
json-stable-stringify "^1.0.1"
-ajv@^5.0.0:
- version "5.2.2"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
- dependencies:
- co "^4.6.0"
- fast-deep-equal "^1.0.0"
- json-schema-traverse "^0.3.0"
- json-stable-stringify "^1.0.1"
-
ajv@^5.1.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
@@ -218,7 +201,7 @@ ansi-align@^2.0.0:
dependencies:
string-width "^2.0.0"
-ansi-escapes@^1.1.0:
+ansi-escapes@^1.0.0, ansi-escapes@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
@@ -242,12 +225,20 @@ ansi-styles@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
-ansi-styles@^3.1.0, ansi-styles@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
dependencies:
color-convert "^1.9.0"
+ansi-styles@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
+
+any-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
+
anymatch@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
@@ -303,9 +294,9 @@ arr-union@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
-array-filter@~0.0.0:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
+array-differ@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031"
array-find-index@^1.0.1:
version "1.0.2"
@@ -330,14 +321,6 @@ array-includes@^3.0.3:
define-properties "^1.1.2"
es-abstract "^1.7.0"
-array-map@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
-
-array-reduce@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
-
array-slice@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
@@ -388,7 +371,7 @@ assert-plus@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
-assert@^1.1.1, assert@^1.4.0:
+assert@^1.1.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
dependencies:
@@ -398,16 +381,18 @@ assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ast-types@0.10.1:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd"
+
+ast-types@0.11.3:
+ version "0.11.3"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8"
+
ast-types@0.x.x:
version "0.11.1"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.1.tgz#5bb3a8d5ba292c3f4ae94d46df37afc30300b990"
-astw@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917"
- dependencies:
- acorn "^4.0.3"
-
async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -416,25 +401,21 @@ async-limiter@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
-async@1.x, async@^1.4.0, async@^1.5.2:
+async@1.x, async@^1.4.0, async@^1.5.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@^2.1.2, async@^2.1.4:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
- dependencies:
- lodash "^4.14.0"
-
-async@^2.4.1:
+async@^2.0.0, async@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
lodash "^4.14.0"
-async@~0.9.0:
- version "0.9.2"
- resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
+async@^2.1.4:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
+ dependencies:
+ lodash "^4.14.0"
async@~2.1.2:
version "2.1.5"
@@ -477,9 +458,9 @@ aws4@^1.2.1, aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
-axios-mock-adapter@^1.10.0:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d"
+axios-mock-adapter@^1.15.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.15.0.tgz#fbc06825d8302c95c3334d21023bba996255d45d"
dependencies:
deep-equal "^1.0.1"
@@ -496,7 +477,7 @@ axios@^0.17.1:
follow-redirects "^1.2.5"
is-buffer "^1.1.5"
-babel-code-frame@6.26.0, babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
+babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
dependencies:
@@ -504,9 +485,9 @@ babel-code-frame@6.26.0, babel-code-frame@^6.16.0, babel-code-frame@^6.26.0:
esutils "^2.0.2"
js-tokens "^3.0.2"
-babel-core@^6.26.0:
- version "6.26.0"
- resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
+babel-core@^6.26.0, babel-core@^6.26.3:
+ version "6.26.3"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207"
dependencies:
babel-code-frame "^6.26.0"
babel-generator "^6.26.0"
@@ -518,15 +499,15 @@ babel-core@^6.26.0:
babel-traverse "^6.26.0"
babel-types "^6.26.0"
babylon "^6.18.0"
- convert-source-map "^1.5.0"
- debug "^2.6.8"
+ convert-source-map "^1.5.1"
+ debug "^2.6.9"
json5 "^0.5.1"
lodash "^4.17.4"
minimatch "^3.0.4"
path-is-absolute "^1.0.1"
- private "^0.1.7"
+ private "^0.1.8"
slash "^1.0.0"
- source-map "^0.5.6"
+ source-map "^0.5.7"
babel-eslint@^8.0.2:
version "8.0.2"
@@ -668,9 +649,9 @@ babel-helpers@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
-babel-loader@^7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126"
+babel-loader@^7.1.4:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.4.tgz#e3463938bd4e6d55d1c174c5485d406a188ed015"
dependencies:
find-cache-dir "^1.0.0"
loader-utils "^1.0.2"
@@ -688,13 +669,18 @@ babel-plugin-check-es2015-constants@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-istanbul@^4.1.5:
- version "4.1.5"
- resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e"
+babel-plugin-istanbul@^4.1.6:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45"
dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
find-up "^2.1.0"
- istanbul-lib-instrument "^1.7.5"
- test-exclude "^4.1.1"
+ istanbul-lib-instrument "^1.10.1"
+ test-exclude "^4.2.1"
+
+babel-plugin-rewire@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.1.0.tgz#a6b966d9d8c06c03d95dcda2eec4e2521519549b"
babel-plugin-syntax-async-functions@^6.8.0:
version "6.13.0"
@@ -704,6 +690,10 @@ babel-plugin-syntax-async-generators@^6.5.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
+babel-plugin-syntax-class-constructor-call@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416"
+
babel-plugin-syntax-class-properties@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
@@ -720,7 +710,15 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
-babel-plugin-syntax-object-rest-spread@^6.8.0:
+babel-plugin-syntax-export-extensions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
+
+babel-plugin-syntax-flow@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+
+babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -744,6 +742,14 @@ babel-plugin-transform-async-to-generator@^6.24.1:
babel-plugin-syntax-async-functions "^6.8.0"
babel-runtime "^6.22.0"
+babel-plugin-transform-class-constructor-call@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9"
+ dependencies:
+ babel-plugin-syntax-class-constructor-call "^6.18.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
babel-plugin-transform-class-properties@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
@@ -946,6 +952,20 @@ babel-plugin-transform-exponentiation-operator@^6.24.1:
babel-plugin-syntax-exponentiation-operator "^6.8.0"
babel-runtime "^6.22.0"
+babel-plugin-transform-export-extensions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653"
+ dependencies:
+ babel-plugin-syntax-export-extensions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-flow-strip-types@^6.8.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
+ dependencies:
+ babel-plugin-syntax-flow "^6.18.0"
+ babel-runtime "^6.22.0"
+
babel-plugin-transform-object-rest-spread@^6.22.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921"
@@ -966,7 +986,7 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
-babel-preset-es2015@^6.24.1:
+babel-preset-es2015@^6.24.1, babel-preset-es2015@^6.9.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
dependencies:
@@ -1016,6 +1036,14 @@ babel-preset-latest@^6.24.1:
babel-preset-es2016 "^6.24.1"
babel-preset-es2017 "^6.24.1"
+babel-preset-stage-1@^6.5.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0"
+ dependencies:
+ babel-plugin-transform-class-constructor-call "^6.24.1"
+ babel-plugin-transform-export-extensions "^6.22.0"
+ babel-preset-stage-2 "^6.24.1"
+
babel-preset-stage-2@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
@@ -1035,7 +1063,7 @@ babel-preset-stage-3@^6.24.1:
babel-plugin-transform-exponentiation-operator "^6.24.1"
babel-plugin-transform-object-rest-spread "^6.22.0"
-babel-register@^6.26.0:
+babel-register@^6.26.0, babel-register@^6.9.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
dependencies:
@@ -1047,7 +1075,7 @@ babel-register@^6.26.0:
mkdirp "^0.5.1"
source-map-support "^0.4.15"
-babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
+babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies:
@@ -1091,10 +1119,14 @@ babylon@7.0.0-beta.32, babylon@^7.0.0-beta.31:
version "7.0.0-beta.32"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.32.tgz#e9033cb077f64d6895f4125968b37dc0a8c3bc6e"
-babylon@^6.18.0:
+babylon@^6.17.3, babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+babylon@^7.0.0-beta.30:
+ version "7.0.0-beta.44"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d"
+
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@@ -1163,6 +1195,10 @@ binary-extensions@^1.0.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+binaryextensions@2:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935"
+
bitsyntax@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.0.4.tgz#eb10cc6f82b8c490e3e85698f07e83d46e0cba82"
@@ -1196,11 +1232,7 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
-bluebird@^3.1.1:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
-
-bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.1:
+bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.4.6, bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@@ -1252,9 +1284,9 @@ boom@5.x.x:
dependencies:
hoek "4.x.x"
-bootstrap-sass@^3.3.6:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
+bootstrap@4.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.0.tgz#110b05c31a236d56dbc9adcda6dd16f53738a28a"
boxen@^1.2.1:
version "1.3.0"
@@ -1268,14 +1300,7 @@ boxen@^1.2.1:
term-size "^1.2.0"
widest-line "^2.0.0"
-brace-expansion@^1.0.0, brace-expansion@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-brace-expansion@^1.1.7:
+brace-expansion@^1.1.7, brace-expansion@^1.1.8:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
@@ -1317,23 +1342,6 @@ brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
-browser-pack@^6.0.1:
- version "6.0.4"
- resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.4.tgz#9a73beb3b48f9e36868be007b64400102c04a99f"
- dependencies:
- JSONStream "^1.0.3"
- combine-source-map "~0.8.0"
- defined "^1.0.0"
- safe-buffer "^5.1.1"
- through2 "^2.0.0"
- umd "^3.0.0"
-
-browser-resolve@^1.11.0, browser-resolve@^1.7.0:
- version "1.11.2"
- resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
- dependencies:
- resolve "1.1.7"
-
browserify-aes@^1.0.0, browserify-aes@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f"
@@ -1386,64 +1394,6 @@ browserify-zlib@^0.1.4:
dependencies:
pako "~0.2.0"
-browserify-zlib@~0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
- dependencies:
- pako "~1.0.5"
-
-browserify@^14.5.0:
- version "14.5.0"
- resolved "https://registry.yarnpkg.com/browserify/-/browserify-14.5.0.tgz#0bbbce521acd6e4d1d54d8e9365008efb85a9cc5"
- dependencies:
- JSONStream "^1.0.3"
- assert "^1.4.0"
- browser-pack "^6.0.1"
- browser-resolve "^1.11.0"
- browserify-zlib "~0.2.0"
- buffer "^5.0.2"
- cached-path-relative "^1.0.0"
- concat-stream "~1.5.1"
- console-browserify "^1.1.0"
- constants-browserify "~1.0.0"
- crypto-browserify "^3.0.0"
- defined "^1.0.0"
- deps-sort "^2.0.0"
- domain-browser "~1.1.0"
- duplexer2 "~0.1.2"
- events "~1.1.0"
- glob "^7.1.0"
- has "^1.0.0"
- htmlescape "^1.1.0"
- https-browserify "^1.0.0"
- inherits "~2.0.1"
- insert-module-globals "^7.0.0"
- labeled-stream-splicer "^2.0.0"
- module-deps "^4.0.8"
- os-browserify "~0.3.0"
- parents "^1.0.1"
- path-browserify "~0.0.0"
- process "~0.11.0"
- punycode "^1.3.2"
- querystring-es3 "~0.2.0"
- read-only-stream "^2.0.0"
- readable-stream "^2.0.2"
- resolve "^1.1.4"
- shasum "^1.0.0"
- shell-quote "^1.6.1"
- stream-browserify "^2.0.0"
- stream-http "^2.0.0"
- string_decoder "~1.0.0"
- subarg "^1.0.0"
- syntax-error "^1.1.1"
- through2 "^2.0.0"
- timers-browserify "^1.0.1"
- tty-browserify "~0.0.0"
- url "~0.11.0"
- util "~0.10.1"
- vm-browserify "~0.0.1"
- xtend "^4.0.0"
-
browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
version "1.7.7"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9"
@@ -1471,13 +1421,6 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
-buffer@^5.0.2:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.1.0.tgz#c913e43678c7cb7c8bd16afbcddb6c5505e8f9fe"
- dependencies:
- base64-js "^1.0.2"
- ieee754 "^1.1.4"
-
buildmail@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/buildmail/-/buildmail-4.0.1.tgz#877f7738b78729871c9a105e3b837d2be11a7a72"
@@ -1506,7 +1449,7 @@ bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
-cacache@^10.0.1:
+cacache@^10.0.1, cacache@^10.0.4:
version "10.0.4"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
dependencies:
@@ -1538,9 +1481,30 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"
-cached-path-relative@^1.0.0:
+cache-loader@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-1.2.2.tgz#6d5c38ded959a09cc5d58190ab5af6f73bd353f5"
+ dependencies:
+ loader-utils "^1.1.0"
+ mkdirp "^0.5.1"
+ neo-async "^2.5.0"
+ schema-utils "^0.4.2"
+
+cacheable-request@^2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-2.1.4.tgz#0d808801b6342ad33c91df9d0b44dc09b91e5c3d"
+ dependencies:
+ clone-response "1.0.2"
+ get-stream "3.0.0"
+ http-cache-semantics "3.8.1"
+ keyv "3.0.0"
+ lowercase-keys "1.0.0"
+ normalize-url "2.0.1"
+ responselike "1.0.2"
+
+call-me-maybe@^1.0.1:
version "1.0.1"
- resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7"
+ resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
caller-path@^0.1.0:
version "0.1.0"
@@ -1571,10 +1535,6 @@ camelcase@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
-camelcase@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
-
camelcase@^4.0.0, camelcase@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
@@ -1611,7 +1571,7 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
-chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies:
@@ -1621,26 +1581,38 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
- ansi-styles "^3.1.0"
+ ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
- supports-color "^4.0.0"
+ supports-color "^5.3.0"
-chalk@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796"
+chalk@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
dependencies:
- ansi-styles "^3.2.0"
+ ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
- supports-color "^5.2.0"
+ supports-color "^5.3.0"
+
+chalk@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
+ dependencies:
+ ansi-styles "~1.0.0"
+ has-color "~0.1.0"
+ strip-ansi "~0.1.0"
chardet@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
+"charenc@>= 0.0.1":
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
+
chart.js@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-1.0.2.tgz#ad57d2229cfd8ccf5955147e8121b4911e69dfe7"
@@ -1649,7 +1621,7 @@ check-types@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
-chokidar@^1.4.1, chokidar@^1.7.0:
+chokidar@^1.4.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
dependencies:
@@ -1686,6 +1658,10 @@ chownr@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181"
+chrome-trace-event@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-0.1.2.tgz#90f36885d5345a50621332f0717b595883d5d982"
+
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
@@ -1724,7 +1700,7 @@ cli-boxes@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
-cli-cursor@^1.0.1:
+cli-cursor@^1.0.1, cli-cursor@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
dependencies:
@@ -1736,6 +1712,23 @@ cli-cursor@^2.1.0:
dependencies:
restore-cursor "^2.0.0"
+cli-spinners@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
+
+cli-table@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
+ dependencies:
+ colors "1.0.3"
+
+cli-truncate@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
+ dependencies:
+ slice-ansi "0.0.4"
+ string-width "^1.0.1"
+
cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
@@ -1756,18 +1749,52 @@ cliui@^2.1.0:
right-align "^0.1.1"
wordwrap "0.0.2"
-cliui@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+cliui@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc"
dependencies:
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
+clone-buffer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
+
+clone-response@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
+ dependencies:
+ mimic-response "^1.0.0"
+
+clone-stats@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
+
+clone-stats@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
+
+clone@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f"
+
clone@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+clone@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
+
+cloneable-readable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117"
+ dependencies:
+ inherits "^2.0.1"
+ process-nextick-args "^1.0.6"
+ through2 "^2.0.1"
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1825,7 +1852,11 @@ colormin@^1.0.5:
css-color-names "0.0.4"
has "^1.0.1"
-colors@^1.1.0, colors@~1.1.2:
+colors@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
+
+colors@^1.1.0, colors@^1.1.2, colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -1835,24 +1866,6 @@ combine-lists@^1.0.0:
dependencies:
lodash "^4.5.0"
-combine-source-map@~0.7.1:
- version "0.7.2"
- resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e"
- dependencies:
- convert-source-map "~1.1.0"
- inline-source-map "~0.6.0"
- lodash.memoize "~3.0.3"
- source-map "~0.5.3"
-
-combine-source-map@~0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b"
- dependencies:
- convert-source-map "~1.1.0"
- inline-source-map "~0.6.0"
- lodash.memoize "~3.0.3"
- source-map "~0.5.3"
-
combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
@@ -1863,6 +1876,10 @@ commander@^2.13.0, commander@^2.15.1, commander@^2.9.0:
version "2.15.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
+commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
+
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -1885,13 +1902,13 @@ compressible@~2.0.10:
dependencies:
mime-db ">= 1.29.0 < 2"
-compression-webpack-plugin@^1.1.7:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.7.tgz#b0dfb97cf1d26baab997b584b8c36fe91872abe2"
+compression-webpack-plugin@^1.1.11:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.11.tgz#8384c7a6ead1d2e2efb190bdfcdcf35878ed8266"
dependencies:
- async "^2.4.1"
cacache "^10.0.1"
find-cache-dir "^1.0.0"
+ neo-async "^2.5.0"
serialize-javascript "^1.4.0"
webpack-sources "^1.0.1"
@@ -1919,14 +1936,6 @@ concat-stream@^1.5.0, concat-stream@^1.5.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
-concat-stream@~1.5.0, concat-stream@~1.5.1:
- version "1.5.2"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266"
- dependencies:
- inherits "~2.0.1"
- readable-stream "~2.0.0"
- typedarray "~0.0.5"
-
configstore@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.1.tgz#094ee662ab83fad9917678de114faaea8fcdca90"
@@ -1961,13 +1970,13 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
-consolidate@^0.14.0:
- version "0.14.5"
- resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.14.5.tgz#5a25047bc76f73072667c8cb52c989888f494c63"
+consolidate@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
dependencies:
bluebird "^3.1.1"
-constants-browserify@^1.0.0, constants-browserify@~1.0.0:
+constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -1983,13 +1992,9 @@ content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
-convert-source-map@^1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
-
-convert-source-map@~1.1.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
+convert-source-map@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
cookie-signature@1.0.6:
version "1.0.6"
@@ -2014,31 +2019,23 @@ copy-descriptor@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
-copy-webpack-plugin@^4.4.1:
- version "4.4.1"
- resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.4.1.tgz#1e8c366211db6dc2ddee40e5a3e4fc661dd149e8"
+copy-webpack-plugin@^4.5.1:
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c"
dependencies:
- cacache "^10.0.1"
+ cacache "^10.0.4"
find-cache-dir "^1.0.0"
globby "^7.1.1"
is-glob "^4.0.0"
- loader-utils "^0.2.15"
+ loader-utils "^1.1.0"
minimatch "^3.0.4"
p-limit "^1.0.0"
serialize-javascript "^1.4.0"
-core-js@^2.2.0:
+core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0:
version "2.5.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
-core-js@^2.4.0, core-js@^2.5.0:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
-
-core-js@^2.4.1:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
-
core-js@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
@@ -2047,17 +2044,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
-cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.1.1.tgz#817f2c2039347a1e9bf7d090c0923e53f749ca82"
- dependencies:
- js-yaml "^3.4.3"
- minimist "^1.2.0"
- object-assign "^4.1.0"
- os-homedir "^1.0.1"
- parse-json "^2.2.0"
- require-from-string "^1.1.0"
-
create-ecdh@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -2097,7 +2083,7 @@ cropper@^2.3.0:
dependencies:
jquery ">= 1.9.1"
-cross-spawn@5.1.0, cross-spawn@^5.0.1:
+cross-spawn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
dependencies:
@@ -2105,6 +2091,20 @@ cross-spawn@5.1.0, cross-spawn@^5.0.1:
shebang-command "^1.2.0"
which "^1.2.9"
+cross-spawn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+"crypt@>= 0.0.1":
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
+
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@@ -2117,7 +2117,7 @@ cryptiles@3.x.x:
dependencies:
boom "5.x.x"
-crypto-browserify@^3.0.0:
+crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
dependencies:
@@ -2133,21 +2133,6 @@ crypto-browserify@^3.0.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
-crypto-browserify@^3.11.0:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522"
- dependencies:
- browserify-cipher "^1.0.0"
- browserify-sign "^4.0.0"
- create-ecdh "^4.0.0"
- create-hash "^1.1.0"
- create-hmac "^1.1.0"
- diffie-hellman "^5.0.0"
- inherits "^2.0.1"
- pbkdf2 "^3.0.3"
- public-encrypt "^4.0.0"
- randombytes "^2.0.0"
-
crypto-random-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
@@ -2156,9 +2141,9 @@ css-color-names@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
-css-loader@^0.28.9:
- version "0.28.9"
- resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.9.tgz#68064b85f4e271d7ce4c48a58300928e535d1c95"
+css-loader@^0.28.11:
+ version "0.28.11"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
dependencies:
babel-code-frame "^6.26.0"
css-selector-tokenizer "^0.7.0"
@@ -2379,6 +2364,10 @@ dagre-layout@^0.8.0:
graphlib "^2.1.1"
lodash "^4.17.4"
+dargs@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829"
+
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -2389,6 +2378,10 @@ data-uri-to-buffer@1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
+date-fns@^1.27.2:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
+
date-format@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8"
@@ -2397,11 +2390,15 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+dateformat@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
+
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
-debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@~2.6.4, debug@~2.6.6:
+debug@2, debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9, debug@~2.6.4, debug@~2.6.6:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
@@ -2413,7 +2410,7 @@ debug@2.2.0, debug@~2.2.0:
dependencies:
ms "0.7.1"
-debug@2.6.8, debug@^2.1.1, debug@^2.6.6, debug@^2.6.8:
+debug@2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
@@ -2437,7 +2434,7 @@ decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
-decompress-response@^3.2.0:
+decompress-response@^3.2.0, decompress-response@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
dependencies:
@@ -2447,6 +2444,10 @@ deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+deep-extend@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f"
+
deep-extend@~0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
@@ -2538,15 +2539,6 @@ depd@1.1.1, depd@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
-deps-sort@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5"
- dependencies:
- JSONStream "^1.0.3"
- shasum "^1.0.0"
- subarg "^1.0.0"
- through2 "^2.0.0"
-
des.js@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -2558,6 +2550,10 @@ destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+detect-conflict@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e"
+
detect-indent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
@@ -2572,28 +2568,18 @@ detect-node@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
-detect-port-alt@1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.5.tgz#a1aa8fc805a4a5df9b905b7ddc7eed036bcce889"
- dependencies:
- address "^1.0.1"
- debug "^2.6.0"
-
-detective@^4.0.0:
- version "4.7.1"
- resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e"
- dependencies:
- acorn "^5.2.1"
- defined "^1.0.0"
-
di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-diff@^3.4.0:
+diff@^3.3.1, diff@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
+diff@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
+
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -2660,7 +2646,7 @@ dom-serializer@0:
domelementtype "~1.1.1"
entities "~1.1.1"
-domain-browser@^1.1.1, domain-browser@~1.1.0:
+domain-browser@^1.1.1:
version "1.1.7"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
@@ -2685,7 +2671,7 @@ domutils@^1.5.1:
dom-serializer "0"
domelementtype "1"
-dot-prop@^4.1.0:
+dot-prop@^4.1.0, dot-prop@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
dependencies:
@@ -2699,12 +2685,6 @@ dropzone@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
-duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
- dependencies:
- readable-stream "^2.0.2"
-
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@@ -2728,6 +2708,10 @@ ecc-jsbn@~0.1.1:
dependencies:
jsbn "~0.1.0"
+editions@^1.3.3:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2736,10 +2720,18 @@ ejs@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
+ejs@^2.5.9:
+ version "2.5.9"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5"
+
electron-to-chromium@^1.2.7:
version "1.3.3"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.3.tgz#651eb63fe89f39db70ffc8dbd5d9b66958bc6a0e"
+elegant-spinner@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
+
elliptic@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@@ -2764,13 +2756,7 @@ encodeurl@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
-end-of-stream@^1.0.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206"
- dependencies:
- once "^1.4.0"
-
-end-of-stream@^1.1.0:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
dependencies:
@@ -2815,14 +2801,13 @@ engine.io@~3.1.0:
optionalDependencies:
uws "~9.14.0"
-enhanced-resolve@^3.4.0:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
+enhanced-resolve@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a"
dependencies:
graceful-fs "^4.1.2"
memory-fs "^0.4.0"
- object-assign "^4.0.1"
- tapable "^0.2.7"
+ tapable "^1.0.0"
enhanced-resolve@~0.9.0:
version "0.9.1"
@@ -2840,18 +2825,41 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+envinfo@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-4.4.2.tgz#472c49f3a8b9bca73962641ce7cb692bf623cd1c"
+
errno@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
dependencies:
prr "~0.0.0"
+errno@^0.1.4:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+ dependencies:
+ prr "~1.0.1"
+
error-ex@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.0.tgz#e67b43f3e82c96ea3a584ffee0b9fc3325d802d9"
dependencies:
is-arrayish "^0.2.1"
+error-ex@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+error@^7.0.2:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
+ dependencies:
+ string-template "~0.2.1"
+ xtend "~4.0.0"
+
es-abstract@^1.7.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
@@ -2930,7 +2938,7 @@ escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
-escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -3095,14 +3103,7 @@ eslint@^3.18.0:
text-table "~0.2.0"
user-home "^2.0.0"
-espree@^3.4.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d"
- dependencies:
- acorn "^5.1.1"
- acorn-jsx "^3.0.0"
-
-espree@^3.5.2:
+espree@^3.4.0, espree@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
dependencies:
@@ -3117,7 +3118,7 @@ esprima@3.x.x, esprima@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
-esprima@^4.0.0:
+esprima@^4.0.0, esprima@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
@@ -3181,7 +3182,7 @@ eventemitter3@1.x.x:
version "1.2.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
-events@^1.0.0, events@~1.1.0:
+events@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@@ -3326,6 +3327,14 @@ external-editor@^2.0.4:
iconv-lite "^0.4.17"
tmp "^0.0.33"
+external-editor@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
+ dependencies:
+ chardet "^0.4.0"
+ iconv-lite "^0.4.17"
+ tmp "^0.0.33"
+
extglob@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
@@ -3357,6 +3366,16 @@ fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+fast-glob@^2.0.2:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.1.tgz#686c2345be88f3741e174add0be6f2e5b6078889"
+ dependencies:
+ "@mrmlnc/readdir-enhanced" "^2.2.1"
+ glob-parent "^3.1.0"
+ is-glob "^4.0.0"
+ merge2 "^1.2.1"
+ micromatch "^3.1.10"
+
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
@@ -3381,7 +3400,7 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
-figures@^1.3.5:
+figures@^1.3.5, figures@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
dependencies:
@@ -3401,9 +3420,9 @@ file-entry-cache@^2.0.0:
flat-cache "^1.2.1"
object-assign "^4.0.1"
-file-loader@^1.1.8:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.8.tgz#a62592ed732667d7482dc3268c381c7f0c913086"
+file-loader@^1.1.11:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8"
dependencies:
loader-utils "^1.0.2"
schema-utils "^0.4.5"
@@ -3423,10 +3442,6 @@ fileset@^2.0.2:
glob "^7.0.3"
minimatch "^3.0.3"
-filesize@3.5.11:
- version "3.5.11"
- resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.5.11.tgz#1919326749433bb3cf77368bd158caabcc19e9ee"
-
filesize@^3.5.11:
version "3.6.0"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.0.tgz#22d079615624bb6fd3c04026120628a41b3f4efa"
@@ -3487,6 +3502,12 @@ find-up@^2.0.0, find-up@^2.1.0:
dependencies:
locate-path "^2.0.0"
+first-chunk-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70"
+ dependencies:
+ readable-stream "^2.0.2"
+
flat-cache@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96"
@@ -3500,6 +3521,10 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+flow-parser@^0.*:
+ version "0.66.0"
+ resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.66.0.tgz#be583fefb01192aa5164415d31a6241b35718983"
+
flush-write-stream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
@@ -3575,7 +3600,7 @@ fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
-from2@^2.1.0:
+from2@^2.1.0, from2@^2.1.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
dependencies:
@@ -3675,7 +3700,7 @@ get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
-get-stream@^3.0.0:
+get-stream@3.0.0, get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -3700,6 +3725,26 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
+gh-got@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0"
+ dependencies:
+ got "^7.0.0"
+ is-plain-obj "^1.1.0"
+
+github-username@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417"
+ dependencies:
+ gh-got "^6.0.0"
+
+glob-all@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab"
+ dependencies:
+ glob "^7.0.5"
+ yargs "~1.2.6"
+
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -3720,6 +3765,10 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
+glob-to-regexp@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+
glob@^5.0.15:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -3741,7 +3790,7 @@ glob@^7.0.0, glob@^7.0.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2:
+glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3758,7 +3807,7 @@ global-dirs@^0.1.0:
dependencies:
ini "^1.3.4"
-global-modules@1.0.0, global-modules@^1.0.0:
+global-modules@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
dependencies:
@@ -3816,6 +3865,18 @@ globby@^7.1.1:
pify "^3.0.0"
slash "^1.0.0"
+globby@^8.0.0:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50"
+ dependencies:
+ array-union "^1.0.1"
+ dir-glob "^2.0.0"
+ fast-glob "^2.0.2"
+ glob "^7.1.2"
+ ignore "^3.3.5"
+ pify "^3.0.0"
+ slash "^1.0.0"
+
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -3838,7 +3899,7 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"
-got@^7.1.0:
+got@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
dependencies:
@@ -3857,6 +3918,28 @@ got@^7.1.0:
url-parse-lax "^1.0.0"
url-to-options "^1.0.1"
+got@^8.0.3, got@^8.2.0:
+ version "8.3.0"
+ resolved "https://registry.yarnpkg.com/got/-/got-8.3.0.tgz#6ba26e75f8a6cc4c6b3eb1fe7ce4fec7abac8533"
+ dependencies:
+ "@sindresorhus/is" "^0.7.0"
+ cacheable-request "^2.1.1"
+ decompress-response "^3.3.0"
+ duplexer3 "^0.1.4"
+ get-stream "^3.0.0"
+ into-stream "^3.1.0"
+ is-retry-allowed "^1.1.0"
+ isurl "^1.0.0-alpha5"
+ lowercase-keys "^1.0.0"
+ mimic-response "^1.0.0"
+ p-cancelable "^0.4.0"
+ p-timeout "^2.0.1"
+ pify "^3.0.0"
+ safe-buffer "^5.1.1"
+ timed-out "^4.0.1"
+ url-parse-lax "^3.0.0"
+ url-to-options "^1.0.1"
+
graceful-fs@^4.1.11, graceful-fs@^4.1.2:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -3867,11 +3950,11 @@ graphlib@^2.1.1:
dependencies:
lodash "^4.11.1"
-gzip-size@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-3.0.0.tgz#546188e9bdc337f673772f81660464b389dce520"
+grouped-queue@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-0.3.3.tgz#c167d2a5319c5a0e0964ef6a25b7c2df8996c85c"
dependencies:
- duplexer "^0.1.1"
+ lodash "^4.17.2"
gzip-size@^4.1.0:
version "4.1.0"
@@ -3937,6 +4020,10 @@ has-binary2@~1.0.2:
dependencies:
isarray "2.0.1"
+has-color@~0.1.0:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
+
has-cors@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
@@ -3994,7 +4081,7 @@ has-values@^1.0.0:
is-number "^3.0.0"
kind-of "^4.0.0"
-has@^1.0.0, has@^1.0.1:
+has@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
dependencies:
@@ -4103,10 +4190,6 @@ html-entities@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
-htmlescape@^1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
-
htmlparser2@^3.8.2, htmlparser2@^3.9.0:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -4118,6 +4201,10 @@ htmlparser2@^3.8.2, htmlparser2@^3.9.0:
inherits "^2.0.1"
readable-stream "^2.0.2"
+http-cache-semantics@3.8.1:
+ version "3.8.1"
+ resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
+
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -4139,14 +4226,14 @@ http-proxy-agent@1:
debug "2"
extend "3"
-http-proxy-middleware@~0.17.4:
- version "0.17.4"
- resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
+http-proxy-middleware@~0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab"
dependencies:
http-proxy "^1.16.2"
- is-glob "^3.1.0"
- lodash "^4.17.2"
- micromatch "^2.3.11"
+ is-glob "^4.0.0"
+ lodash "^4.17.5"
+ micromatch "^3.1.9"
http-proxy@^1.13.0, http-proxy@^1.16.2:
version "1.16.2"
@@ -4186,10 +4273,6 @@ https-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
-https-browserify@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
-
https-proxy-agent@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6"
@@ -4228,11 +4311,7 @@ ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
-ignore@^3.2.0:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
-
-ignore@^3.3.5, ignore@^3.3.7:
+ignore@^3.2.0, ignore@^3.3.5, ignore@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
@@ -4268,6 +4347,10 @@ indent-string@^2.1.0:
dependencies:
repeating "^2.0.0"
+indent-string@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
+
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -4303,13 +4386,25 @@ ini@^1.3.4, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-inline-source-map@~0.6.0:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5"
+inquirer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
dependencies:
- source-map "~0.5.3"
+ ansi-escapes "^1.1.0"
+ ansi-regex "^2.0.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ readline2 "^1.0.1"
+ run-async "^0.1.0"
+ rx-lite "^3.1.2"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
-inquirer@3.3.0:
+inquirer@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
dependencies:
@@ -4328,37 +4423,24 @@ inquirer@3.3.0:
strip-ansi "^4.0.0"
through "^2.3.6"
-inquirer@^0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+inquirer@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726"
dependencies:
- ansi-escapes "^1.1.0"
- ansi-regex "^2.0.0"
- chalk "^1.0.0"
- cli-cursor "^1.0.1"
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
cli-width "^2.0.0"
- figures "^1.3.5"
+ external-editor "^2.1.0"
+ figures "^2.0.0"
lodash "^4.3.0"
- readline2 "^1.0.1"
- run-async "^0.1.0"
- rx-lite "^3.1.2"
- string-width "^1.0.1"
- strip-ansi "^3.0.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rxjs "^5.5.2"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
through "^2.3.6"
-insert-module-globals@^7.0.0:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3"
- dependencies:
- JSONStream "^1.0.3"
- combine-source-map "~0.7.1"
- concat-stream "~1.5.1"
- is-buffer "^1.1.0"
- lexical-scope "^1.2.0"
- process "~0.11.0"
- through2 "^2.0.0"
- xtend "^4.0.0"
-
internal-ip@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
@@ -4369,6 +4451,17 @@ interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
+interpret@^1.0.4:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
+
+into-stream@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
+ dependencies:
+ from2 "^2.1.1"
+ p-is-promise "^1.1.0"
+
invariant@^2.2.0, invariant@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
@@ -4424,14 +4517,10 @@ is-binary-path@^1.0.0:
dependencies:
binary-extensions "^1.0.0"
-is-buffer@^1.1.0:
+is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
-is-buffer@^1.1.5:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
-
is-builtin-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
@@ -4547,16 +4636,7 @@ is-my-ip-valid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
-is-my-json-valid@^2.10.0:
- version "2.16.0"
- resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693"
- dependencies:
- generate-function "^2.0.0"
- generate-object-property "^1.1.0"
- jsonpointer "^4.0.0"
- xtend "^4.0.0"
-
-is-my-json-valid@^2.12.4:
+is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
version "2.17.2"
resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz#6b2103a288e94ef3de5cf15d29dd85fc4b78d65c"
dependencies:
@@ -4598,6 +4678,12 @@ is-object@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
+is-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2"
+ dependencies:
+ symbol-observable "^0.2.2"
+
is-odd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24"
@@ -4668,13 +4754,15 @@ is-resolvable@^1.0.0:
dependencies:
tryit "^1.0.1"
-is-retry-allowed@^1.0.0:
+is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
-is-root@1.0.0:
+is-scoped@^1.0.0:
version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-root/-/is-root-1.0.0.tgz#07b6c233bc394cd9d02ba15c966bd6660d6342d5"
+ resolved "https://registry.yarnpkg.com/is-scoped/-/is-scoped-1.0.0.tgz#449ca98299e713038256289ecb2b540dc437cb30"
+ dependencies:
+ scoped-regex "^1.0.0"
is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
@@ -4716,7 +4804,7 @@ is-wsl@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
-isarray@0.0.1, isarray@~0.0.1:
+isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -4728,14 +4816,10 @@ isarray@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
-isbinaryfile@^3.0.0:
+isbinaryfile@^3.0.0, isbinaryfile@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
-isexe@^1.1.1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
-
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -4774,13 +4858,29 @@ istanbul-lib-coverage@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
+istanbul-lib-coverage@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
+
istanbul-lib-hook@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b"
dependencies:
append-transform "^0.4.0"
-istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.9.1:
+istanbul-lib-instrument@^1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b"
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.2.0"
+ semver "^5.3.0"
+
+istanbul-lib-instrument@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e"
dependencies:
@@ -4836,6 +4936,14 @@ istanbul@^0.4.5:
which "^1.1.1"
wordwrap "^1.0.0"
+istextorbinary@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53"
+ dependencies:
+ binaryextensions "2"
+ editions "^1.3.3"
+ textextensions "2"
+
isurl@^1.0.0-alpha5:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67"
@@ -4881,7 +4989,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
-js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
+js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
dependencies:
@@ -4899,6 +5007,46 @@ jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+jscodeshift@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.4.1.tgz#da91a1c2eccfa03a3387a21d39948e251ced444a"
+ dependencies:
+ async "^1.5.0"
+ babel-plugin-transform-flow-strip-types "^6.8.0"
+ babel-preset-es2015 "^6.9.0"
+ babel-preset-stage-1 "^6.5.0"
+ babel-register "^6.9.0"
+ babylon "^6.17.3"
+ colors "^1.1.2"
+ flow-parser "^0.*"
+ lodash "^4.13.1"
+ micromatch "^2.3.7"
+ node-dir "0.1.8"
+ nomnom "^1.8.1"
+ recast "^0.12.5"
+ temp "^0.8.1"
+ write-file-atomic "^1.2.0"
+
+jscodeshift@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.5.0.tgz#bdb7b6cc20dd62c16aa728c3fa2d2fe66ca7c748"
+ dependencies:
+ babel-plugin-transform-flow-strip-types "^6.8.0"
+ babel-preset-es2015 "^6.9.0"
+ babel-preset-stage-1 "^6.5.0"
+ babel-register "^6.9.0"
+ babylon "^7.0.0-beta.30"
+ colors "^1.1.2"
+ flow-parser "^0.*"
+ lodash "^4.13.1"
+ micromatch "^2.3.7"
+ neo-async "^2.5.0"
+ node-dir "0.1.8"
+ nomnom "^1.8.1"
+ recast "^0.14.1"
+ temp "^0.8.1"
+ write-file-atomic "^1.2.0"
+
jsesc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
@@ -4907,9 +5055,13 @@ jsesc@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
-json-loader@^0.5.4:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
+json-buffer@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
+
+json-parse-better-errors@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
json-schema-traverse@^0.3.0:
version "0.3.1"
@@ -4925,12 +5077,6 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
dependencies:
jsonify "~0.0.0"
-json-stable-stringify@~0.0.0:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45"
- dependencies:
- jsonify "~0.0.0"
-
json-stringify-safe@5.0.x, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -4947,10 +5093,6 @@ jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
-jsonparse@^1.2.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
-
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -4985,9 +5127,9 @@ karma-chrome-launcher@^2.2.0:
fs-access "^1.0.0"
which "^1.2.1"
-karma-coverage-istanbul-reporter@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.1.tgz#2b42d145ddbb4868d85d52888c495a21c61d873c"
+karma-coverage-istanbul-reporter@^1.4.2:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-1.4.2.tgz#a8d0c8815c7d6f6cea15a394a7c4b39ef151a939"
dependencies:
istanbul-api "^1.1.14"
minimatch "^3.0.4"
@@ -5010,23 +5152,23 @@ karma-sourcemap-loader@^0.3.7:
dependencies:
graceful-fs "^4.1.2"
-karma-webpack@2.0.7:
- version "2.0.7"
- resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.7.tgz#dc3a492b478f10e8e3ccb9f58171b623f7070a1f"
+karma-webpack@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-3.0.0.tgz#bf009c5b73c667c11c015717e9e520f581317c44"
dependencies:
- async "~0.9.0"
- loader-utils "^0.2.5"
- lodash "^3.8.0"
+ async "^2.0.0"
+ babel-runtime "^6.0.0"
+ loader-utils "^1.0.0"
+ lodash "^4.0.0"
source-map "^0.5.6"
- webpack-dev-middleware "^1.12.0"
+ webpack-dev-middleware "^2.0.6"
-karma@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/karma/-/karma-2.0.0.tgz#a02698dd7f0f05ff5eb66ab8f65582490b512e58"
+karma@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-2.0.2.tgz#4d2db9402850a66551fa784b0164fb0824ed8c4b"
dependencies:
bluebird "^3.3.0"
body-parser "^1.16.1"
- browserify "^14.5.0"
chokidar "^1.4.1"
colors "^1.1.0"
combine-lists "^1.0.0"
@@ -5051,7 +5193,7 @@ karma@^2.0.0:
socket.io "2.0.4"
source-map "^0.6.1"
tmp "0.0.33"
- useragent "^2.1.12"
+ useragent "2.2.1"
katex@^0.8.3:
version "0.8.3"
@@ -5059,6 +5201,12 @@ katex@^0.8.3:
dependencies:
match-at "^0.1.0"
+keyv@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373"
+ dependencies:
+ json-buffer "3.0.0"
+
killable@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
@@ -5083,14 +5231,6 @@ kind-of@^6.0.0, kind-of@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
-labeled-stream-splicer@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59"
- dependencies:
- inherits "^2.0.1"
- isarray "~0.0.1"
- stream-splicer "^2.0.0"
-
latest-version@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
@@ -5120,12 +5260,6 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
-lexical-scope@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4"
- dependencies:
- astw "^2.0.0"
-
libbase64@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-0.1.0.tgz#62351a839563ac5ff5bd26f12f60e9830bb751e6"
@@ -5148,6 +5282,54 @@ lie@~3.1.0:
dependencies:
immediate "~3.0.5"
+listr-silent-renderer@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
+
+listr-update-renderer@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ elegant-spinner "^1.0.1"
+ figures "^1.7.0"
+ indent-string "^3.0.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ strip-ansi "^3.0.1"
+
+listr-verbose-renderer@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35"
+ dependencies:
+ chalk "^1.1.3"
+ cli-cursor "^1.0.2"
+ date-fns "^1.27.2"
+ figures "^1.7.0"
+
+listr@^0.13.0:
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ figures "^1.7.0"
+ indent-string "^2.1.0"
+ is-observable "^0.2.0"
+ is-promise "^2.1.0"
+ is-stream "^1.1.0"
+ listr-silent-renderer "^1.1.1"
+ listr-update-renderer "^0.4.0"
+ listr-verbose-renderer "^0.4.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ ora "^0.2.3"
+ p-map "^1.1.1"
+ rxjs "^5.4.2"
+ stream-to-observable "^0.2.0"
+ strip-ansi "^3.0.1"
+
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -5158,28 +5340,19 @@ load-json-file@^1.0.0:
pinkie-promise "^2.0.0"
strip-bom "^2.0.0"
-load-json-file@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+load-json-file@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
dependencies:
graceful-fs "^4.1.2"
- parse-json "^2.2.0"
- pify "^2.0.0"
+ parse-json "^4.0.0"
+ pify "^3.0.0"
strip-bom "^3.0.0"
loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@^0.2.15, loader-utils@^0.2.5:
- version "0.2.16"
- resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
- dependencies:
- big.js "^3.1.3"
- emojis-list "^2.0.0"
- json5 "^0.5.0"
- object-assign "^4.0.1"
-
loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
@@ -5233,10 +5406,6 @@ lodash.deburr@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
-lodash.endswith@^4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09"
-
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
@@ -5252,14 +5421,6 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
-lodash.isfunction@^3.0.8:
- version "3.0.9"
- resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
-
-lodash.isstring@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
-
lodash.kebabcase@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.0.1.tgz#5e63bc9aa2a5562ff3b97ca7af2f803de1bcb90e"
@@ -5271,10 +5432,6 @@ lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
-lodash.memoize@~3.0.3:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
-
lodash.mergewith@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
@@ -5286,10 +5443,6 @@ lodash.snakecase@4.0.1:
lodash.deburr "^4.0.0"
lodash.words "^4.0.0"
-lodash.startswith@^4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c"
-
lodash.uniq@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@@ -5298,24 +5451,43 @@ lodash.words@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
-lodash@4.17.4, lodash@^4.11.1, lodash@^4.17.2, lodash@^4.2.0, lodash@^4.3.0:
+lodash@4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-lodash@^3.8.0:
- version "3.10.1"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-
-lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
+lodash@^4.17.10:
+ version "4.17.10"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
+
+log-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
+ dependencies:
+ chalk "^1.0.0"
+
log-symbols@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6"
dependencies:
chalk "^2.0.1"
+log-symbols@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+ dependencies:
+ chalk "^2.0.1"
+
+log-update@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"
+ dependencies:
+ ansi-escapes "^1.0.0"
+ cli-cursor "^1.0.2"
+
log4js@^2.3.9:
version "2.5.3"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-2.5.3.tgz#38bb7bde5e9c1c181bd75e8bc128c5cd0409caf1"
@@ -5347,6 +5519,10 @@ loglevel@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd"
+loglevelnext@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.3.tgz#0f69277e73bbbf2cd61b94d82313216bf87ac66e"
+
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -5357,24 +5533,35 @@ loose-envify@^1.0.0:
dependencies:
js-tokens "^3.0.0"
-loud-rejection@^1.0.0:
+loud-rejection@^1.0.0, loud-rejection@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
dependencies:
currently-unhandled "^0.4.1"
signal-exit "^3.0.0"
-lowercase-keys@^1.0.0:
+lowercase-keys@1.0.0, lowercase-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
-lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.1:
+lru-cache@2.2.x:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
+
+lru-cache@^4.0.1, lru-cache@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
+lru-cache@^4.1.2:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
lru-cache@~2.6.5:
version "2.6.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5"
@@ -5410,6 +5597,12 @@ make-dir@^1.0.0:
dependencies:
pify "^2.3.0"
+make-dir@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
+ dependencies:
+ pify "^3.0.0"
+
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@@ -5451,6 +5644,30 @@ media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+mem-fs-editor@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-4.0.1.tgz#27e6b59df91b37248e9be2145b1bea84695103ed"
+ dependencies:
+ commondir "^1.0.1"
+ deep-extend "^0.5.1"
+ ejs "^2.5.9"
+ glob "^7.0.3"
+ globby "^8.0.0"
+ isbinaryfile "^3.0.2"
+ mkdirp "^0.5.0"
+ multimatch "^2.0.0"
+ rimraf "^2.2.8"
+ through2 "^2.0.0"
+ vinyl "^2.0.1"
+
+mem-fs@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.1.3.tgz#b8ae8d2e3fcb6f5d3f9165c12d4551a065d989cc"
+ dependencies:
+ through2 "^2.0.0"
+ vinyl "^1.1.0"
+ vinyl-file "^2.0.0"
+
mem@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
@@ -5487,11 +5704,21 @@ merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+merge-source-map@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
+ dependencies:
+ source-map "^0.6.1"
+
+merge2@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34"
+
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
-micromatch@^2.1.5, micromatch@^2.3.11:
+micromatch@^2.1.5, micromatch@^2.3.7:
version "2.3.11"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
dependencies:
@@ -5509,6 +5736,24 @@ micromatch@^2.1.5, micromatch@^2.3.11:
parse-glob "^3.0.4"
regex-cache "^0.4.2"
+micromatch@^3.1.10, micromatch@^3.1.9:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
micromatch@^3.1.4:
version "3.1.6"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.6.tgz#8d7c043b48156f408ca07a4715182b79b99420bf"
@@ -5527,6 +5772,24 @@ micromatch@^3.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+micromatch@^3.1.8:
+ version "3.1.9"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -5534,15 +5797,11 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.29.0 < 2":
- version "1.29.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
-
-mime-db@~1.33.0:
+"mime-db@>= 1.29.0 < 2", mime-db@~1.33.0:
version "1.33.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
-mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
+mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7:
version "2.1.18"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8"
dependencies:
@@ -5552,10 +5811,18 @@ mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
-mime@^1.3.4, mime@^1.4.1, mime@^1.5.0:
+mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+mime@^2.0.3:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
+
+mime@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b"
+
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -5578,17 +5845,15 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
dependencies:
brace-expansion "^1.1.7"
-minimatch@3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
- dependencies:
- brace-expansion "^1.0.0"
-
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0:
+minimist@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de"
+
+minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -5624,26 +5889,6 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd
dependencies:
minimist "0.0.8"
-module-deps@^4.0.8:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd"
- dependencies:
- JSONStream "^1.0.3"
- browser-resolve "^1.7.0"
- cached-path-relative "^1.0.0"
- concat-stream "~1.5.0"
- defined "^1.0.0"
- detective "^4.0.0"
- duplexer2 "^0.1.2"
- inherits "^2.0.1"
- parents "^1.0.0"
- readable-stream "^2.0.2"
- resolve "^1.1.3"
- stream-combiner2 "^1.1.1"
- subarg "^1.0.0"
- through2 "^2.0.0"
- xtend "^4.0.0"
-
moment@2.x, moment@^2.18.1:
version "2.19.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"
@@ -5686,6 +5931,15 @@ multicast-dns@^6.0.1:
dns-packet "^1.0.1"
thunky "^0.1.0"
+multimatch@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b"
+ dependencies:
+ array-differ "^1.0.0"
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ minimatch "^3.0.0"
+
mute-stream@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
@@ -5694,10 +5948,6 @@ mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-name-all-modules-plugin@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz#0abfb6ad835718b9fb4def0674e06657a954375c"
-
nan@^2.3.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
@@ -5727,10 +5977,22 @@ negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+neo-async@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f"
+
netmask@~1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
+nice-try@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4"
+
+node-dir@0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.8.tgz#55fb8deb699070707fb67f91a460f0448294c77d"
+
node-forge@0.6.33:
version "0.6.33"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
@@ -5860,20 +6122,28 @@ nodemailer@^2.5.0:
nodemailer-smtp-transport "2.7.2"
socks "1.1.9"
-nodemon@^1.15.1:
- version "1.15.1"
- resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.15.1.tgz#54daa72443d8d5a548f130866b92e65cded0ed58"
+nodemon@^1.17.3:
+ version "1.17.3"
+ resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.17.3.tgz#3b0bbc2ee05ccb43b1aef15ba05c63c7bc9b8530"
dependencies:
chokidar "^2.0.2"
debug "^3.1.0"
ignore-by-default "^1.0.1"
minimatch "^3.0.4"
pstree.remy "^1.1.0"
- semver "^5.4.1"
+ semver "^5.5.0"
+ supports-color "^5.2.0"
touch "^3.1.0"
- undefsafe "^2.0.1"
+ undefsafe "^2.0.2"
update-notifier "^2.3.0"
+nomnom@^1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
+ dependencies:
+ chalk "~0.4.0"
+ underscore "~1.6.0"
+
nopt@3.x:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@@ -5912,6 +6182,14 @@ normalize-range@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+normalize-url@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
+ dependencies:
+ prepend-http "^2.0.0"
+ query-string "^5.0.1"
+ sort-keys "^2.0.0"
+
normalize-url@^1.4.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
@@ -6025,7 +6303,7 @@ opener@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
-opn@5.2.0, opn@^5.1.0:
+opn@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
dependencies:
@@ -6049,6 +6327,15 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
+ora@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
+ dependencies:
+ chalk "^1.1.1"
+ cli-cursor "^1.0.2"
+ cli-spinners "^0.1.2"
+ object-assign "^4.0.1"
+
original@>=0.0.5:
version "1.0.0"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b"
@@ -6059,20 +6346,10 @@ os-browserify@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
-os-browserify@~0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
-
-os-homedir@^1.0.0, os-homedir@^1.0.1:
+os-homedir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-os-locale@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
- dependencies:
- lcid "^1.0.0"
-
os-locale@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
@@ -6096,20 +6373,34 @@ p-cancelable@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
+p-cancelable@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0"
+
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ dependencies:
+ p-reduce "^1.0.0"
+
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
-p-limit@^1.0.0:
+p-is-promise@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e"
+
+p-lazy@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-lazy/-/p-lazy-1.0.0.tgz#ec53c802f2ee3ac28f166cc82d0b2b02de27a835"
+
+p-limit@^1.0.0, p-limit@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
dependencies:
p-try "^1.0.0"
-p-limit@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc"
-
p-locate@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -6120,9 +6411,19 @@ p-map@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+
p-timeout@^1.1.1:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.0.tgz#9820f99434c5817868b4f34809ee5291660d5b6c"
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386"
+ dependencies:
+ p-finally "^1.0.0"
+
+p-timeout@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038"
dependencies:
p-finally "^1.0.0"
@@ -6168,10 +6469,6 @@ pako@~0.2.0:
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
pako@~1.0.2:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.5.tgz#d2205dfe5b9da8af797e7c163db4d1f84e4600bc"
-
-pako@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
@@ -6183,12 +6480,6 @@ parallel-transform@^1.1.0:
inherits "^2.0.3"
readable-stream "^2.1.5"
-parents@^1.0.0, parents@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751"
- dependencies:
- path-platform "~0.11.15"
-
parse-asn1@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
@@ -6214,6 +6505,13 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
parse-passwd@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
@@ -6238,7 +6536,7 @@ pascalcase@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
-path-browserify@0.0.0, path-browserify@~0.0.0:
+path-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
@@ -6264,7 +6562,7 @@ path-is-inside@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
-path-key@^2.0.0:
+path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
@@ -6272,10 +6570,6 @@ path-parse@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
-path-platform@~0.11.15:
- version "0.11.15"
- resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2"
-
path-proxy@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/path-proxy/-/path-proxy-1.0.0.tgz#18e8a36859fc9d2f1a53b48dee138543c020de5e"
@@ -6294,12 +6588,6 @@ path-type@^1.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
-path-type@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
- dependencies:
- pify "^2.0.0"
-
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@@ -6376,6 +6664,10 @@ pluralize@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+popper.js@^1.14.3:
+ version "1.14.3"
+ resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
+
portfinder@^1.0.9:
version "1.0.13"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
@@ -6449,29 +6741,6 @@ postcss-filter-plugins@^2.0.0:
postcss "^5.0.4"
uniqid "^4.0.0"
-postcss-load-config@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a"
- dependencies:
- cosmiconfig "^2.1.0"
- object-assign "^4.1.0"
- postcss-load-options "^1.2.0"
- postcss-load-plugins "^2.3.0"
-
-postcss-load-options@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c"
- dependencies:
- cosmiconfig "^2.1.0"
- object-assign "^4.1.0"
-
-postcss-load-plugins@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92"
- dependencies:
- cosmiconfig "^2.1.1"
- object-assign "^4.1.0"
-
postcss-merge-idents@^2.1.5:
version "2.1.7"
resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270"
@@ -6611,6 +6880,14 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2:
indexes-of "^1.0.1"
uniq "^1.0.1"
+postcss-selector-parser@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
+ dependencies:
+ dot-prop "^4.1.1"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
postcss-svgo@^2.1.1:
version "2.1.6"
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
@@ -6649,7 +6926,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
source-map "^0.5.6"
supports-color "^3.2.3"
-postcss@^6.0.1:
+postcss@^6.0.1, postcss@^6.0.14:
version "6.0.19"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555"
dependencies:
@@ -6657,21 +6934,13 @@ postcss@^6.0.1:
source-map "^0.6.1"
supports-color "^5.2.0"
-postcss@^6.0.14:
- version "6.0.15"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.15.tgz#f460cd6269fede0d1bf6defff0b934a9845d974d"
+postcss@^6.0.20:
+ version "6.0.22"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
dependencies:
- chalk "^2.3.0"
+ chalk "^2.4.1"
source-map "^0.6.1"
- supports-color "^5.1.0"
-
-postcss@^6.0.8:
- version "6.0.14"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885"
- dependencies:
- chalk "^2.3.0"
- source-map "^0.6.1"
- supports-color "^4.4.0"
+ supports-color "^5.4.0"
prelude-ls@~1.1.2:
version "1.1.2"
@@ -6681,6 +6950,10 @@ prepend-http@^1.0.0, prepend-http@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
+prepend-http@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
+
preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
@@ -6689,9 +6962,17 @@ prettier@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
-prettier@^1.7.0:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8"
+prettier@^1.11.1:
+ version "1.12.1"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
+
+prettier@^1.5.3:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93"
+
+pretty-bytes@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
prismjs@^1.6.0:
version "1.6.0"
@@ -6699,11 +6980,11 @@ prismjs@^1.6.0:
optionalDependencies:
clipboard "^1.5.5"
-private@^0.1.6, private@^0.1.7:
+private@^0.1.6, private@^0.1.8, private@~0.1.5:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
-process-nextick-args@~1.0.6:
+process-nextick-args@^1.0.6, process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
@@ -6711,11 +6992,7 @@ process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
-process@^0.11.0:
- version "0.11.9"
- resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1"
-
-process@~0.11.0:
+process@^0.11.0, process@~0.11.0:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -6751,6 +7028,10 @@ prr@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+
ps-tree@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014"
@@ -6796,7 +7077,7 @@ punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
-punycode@1.4.1, punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1:
+punycode@1.4.1, punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
@@ -6831,7 +7112,15 @@ query-string@^4.1.0:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
-querystring-es3@^0.2.0, querystring-es3@~0.2.0:
+query-string@^5.0.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
+ dependencies:
+ decode-uri-component "^0.2.0"
+ object-assign "^4.1.0"
+ strict-uri-encode "^1.0.0"
+
+querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -6894,16 +7183,7 @@ raw-loader@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
-rc@^1.0.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
- dependencies:
- deep-extend "~0.4.0"
- ini "~1.3.0"
- minimist "^1.2.0"
- strip-json-comments "~2.0.1"
-
-rc@^1.1.6, rc@^1.1.7:
+rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
version "1.2.5"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd"
dependencies:
@@ -6912,38 +7192,12 @@ rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-dev-utils@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.0.tgz#425ac7c9c40c2603bc4f7ab8836c1406e96bb473"
- dependencies:
- address "1.0.3"
- babel-code-frame "6.26.0"
- chalk "1.1.3"
- cross-spawn "5.1.0"
- detect-port-alt "1.1.5"
- escape-string-regexp "1.0.5"
- filesize "3.5.11"
- global-modules "1.0.0"
- gzip-size "3.0.0"
- inquirer "3.3.0"
- is-root "1.0.0"
- opn "5.2.0"
- react-error-overlay "^4.0.0"
- recursive-readdir "2.2.1"
- shell-quote "1.6.1"
- sockjs-client "1.1.4"
- strip-ansi "3.0.1"
- text-table "0.2.0"
-
-react-error-overlay@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
-
-read-only-stream@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
+read-chunk@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz#6a04c0928005ed9d42e1a6ac5600e19cbc7ff655"
dependencies:
- readable-stream "^2.0.2"
+ pify "^3.0.0"
+ safe-buffer "^5.1.1"
read-pkg-up@^1.0.1:
version "1.0.1"
@@ -6952,12 +7206,12 @@ read-pkg-up@^1.0.1:
find-up "^1.0.0"
read-pkg "^1.0.0"
-read-pkg-up@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+read-pkg-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
dependencies:
find-up "^2.0.0"
- read-pkg "^2.0.0"
+ read-pkg "^3.0.0"
read-pkg@^1.0.0:
version "1.1.0"
@@ -6967,15 +7221,15 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
-read-pkg@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+read-pkg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
dependencies:
- load-json-file "^2.0.0"
+ load-json-file "^4.0.0"
normalize-package-data "^2.3.2"
- path-type "^2.0.0"
+ path-type "^3.0.0"
-"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.3.0, readable-stream@^2.3.3:
+"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
dependencies:
@@ -6996,19 +7250,7 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
isarray "0.0.1"
string_decoder "~0.10.x"
-readable-stream@^2.0.0, readable-stream@^2.1.0, readable-stream@^2.2.2, readable-stream@^2.2.9:
- version "2.3.3"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.3"
- isarray "~1.0.0"
- process-nextick-args "~1.0.6"
- safe-buffer "~5.1.1"
- string_decoder "~1.0.3"
- util-deprecate "~1.0.1"
-
-readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@~2.0.0, readable-stream@~2.0.5, readable-stream@~2.0.6:
+readable-stream@~2.0.5, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
dependencies:
@@ -7036,18 +7278,31 @@ readline2@^1.0.1:
is-fullwidth-code-point "^1.0.0"
mute-stream "0.0.5"
+recast@^0.12.5:
+ version "0.12.9"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.12.9.tgz#e8e52bdb9691af462ccbd7c15d5a5113647a15f1"
+ dependencies:
+ ast-types "0.10.1"
+ core-js "^2.4.1"
+ esprima "~4.0.0"
+ private "~0.1.5"
+ source-map "~0.6.1"
+
+recast@^0.14.1:
+ version "0.14.7"
+ resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d"
+ dependencies:
+ ast-types "0.11.3"
+ esprima "~4.0.0"
+ private "~0.1.5"
+ source-map "~0.6.1"
+
rechoir@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
dependencies:
resolve "^1.1.6"
-recursive-readdir@2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99"
- dependencies:
- minimatch "3.0.3"
-
redent@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -7107,7 +7362,7 @@ regex-cache@^0.4.2:
dependencies:
is-equal-shallow "^0.1.3"
-regex-not@^1.0.0:
+regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
dependencies:
@@ -7175,6 +7430,14 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
+replace-ext@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
+
+replace-ext@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
+
request@2.75.x:
version "2.75.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93"
@@ -7272,10 +7535,6 @@ require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
-require-from-string@^1.1.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418"
-
require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
@@ -7316,16 +7575,22 @@ resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
-resolve@1.1.7, resolve@1.1.x:
+resolve@1.1.x:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.2.0, resolve@^1.4.0:
+resolve@^1.1.6, resolve@^1.2.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
dependencies:
path-parse "^1.0.5"
+responselike@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
+ dependencies:
+ lowercase-keys "^1.0.0"
+
restore-cursor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
@@ -7362,6 +7627,10 @@ rimraf@^2.2.8:
dependencies:
glob "^7.0.5"
+rimraf@~2.2.6:
+ version "2.2.8"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
+
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
@@ -7375,7 +7644,7 @@ run-async@^0.1.0:
dependencies:
once "^1.3.0"
-run-async@^2.2.0:
+run-async@^2.0.0, run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
dependencies:
@@ -7401,6 +7670,12 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+rxjs@^5.4.2, rxjs@^5.5.2:
+ version "5.5.10"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045"
+ dependencies:
+ symbol-observable "1.0.1"
+
safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -7427,19 +7702,17 @@ sax@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
-schema-utils@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
- dependencies:
- ajv "^5.0.0"
-
-schema-utils@^0.4.3, schema-utils@^0.4.5:
+schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5:
version "0.4.5"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e"
dependencies:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
+scoped-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7468,7 +7741,7 @@ semver-diff@^2.0.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
-semver@^5.1.0, semver@^5.3.0, semver@^5.4.1:
+semver@^5.1.0, semver@^5.3.0, semver@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
@@ -7563,19 +7836,19 @@ setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
-sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4:
+sha.js@^2.4.0, sha.js@^2.4.8:
version "2.4.10"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b"
dependencies:
inherits "^2.0.1"
safe-buffer "^5.0.1"
-shasum@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f"
+sha1@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848"
dependencies:
- json-stable-stringify "~0.0.0"
- sha.js "~2.4.4"
+ charenc ">= 0.0.1"
+ crypt ">= 0.0.1"
shebang-command@^1.2.0:
version "1.2.0"
@@ -7587,15 +7860,6 @@ shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
-shell-quote@1.6.1, shell-quote@^1.6.1:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
- dependencies:
- array-filter "~0.0.0"
- array-map "~0.0.0"
- array-reduce "~0.0.0"
- jsonify "~0.0.0"
-
shelljs@^0.7.5:
version "0.7.8"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
@@ -7604,6 +7868,14 @@ shelljs@^0.7.5:
interpret "^1.0.0"
rechoir "^0.6.2"
+shelljs@^0.8.0:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1"
+ dependencies:
+ glob "^7.0.0"
+ interpret "^1.0.0"
+ rechoir "^0.6.2"
+
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"
@@ -7622,6 +7894,10 @@ slice-ansi@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+slide@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707"
+
smart-buffer@^1.0.13, smart-buffer@^1.0.4:
version "1.1.15"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16"
@@ -7759,6 +8035,12 @@ sort-keys@^1.0.0:
dependencies:
is-plain-obj "^1.0.0"
+sort-keys@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
+ dependencies:
+ is-plain-obj "^1.0.0"
+
source-list-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
@@ -7797,7 +8079,11 @@ source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-source-map@^0.6.1:
+source-map@^0.5.7, source-map@~0.5.3, source-map@~0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
@@ -7807,10 +8093,6 @@ source-map@~0.2.0:
dependencies:
amdefine ">=0.0.4"
-source-map@~0.5.3, source-map@~0.5.6:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
-
spdx-correct@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -7910,20 +8192,17 @@ statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
-stream-browserify@^2.0.0, stream-browserify@^2.0.1:
+stickyfilljs@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/stickyfilljs/-/stickyfilljs-2.0.5.tgz#d229e372d2199ddf5d283bbe34ac1f7d2529c2fc"
+
+stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
dependencies:
inherits "~2.0.1"
readable-stream "^2.0.2"
-stream-combiner2@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe"
- dependencies:
- duplexer2 "~0.1.0"
- readable-stream "^2.0.2"
-
stream-combiner@~0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
@@ -7937,7 +8216,7 @@ stream-each@^1.1.0:
end-of-stream "^1.1.0"
stream-shift "^1.0.0"
-stream-http@^2.0.0:
+stream-http@^2.3.1:
version "2.8.0"
resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
dependencies:
@@ -7947,26 +8226,15 @@ stream-http@^2.0.0:
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
-stream-http@^2.3.1:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3"
- dependencies:
- builtin-status-codes "^3.0.0"
- inherits "^2.0.1"
- readable-stream "^2.1.0"
- to-arraybuffer "^1.0.0"
- xtend "^4.0.0"
-
stream-shift@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
-stream-splicer@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83"
+stream-to-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10"
dependencies:
- inherits "^2.0.1"
- readable-stream "^2.0.2"
+ any-observable "^0.2.0"
streamroller@^0.7.0:
version "0.7.0"
@@ -7981,6 +8249,10 @@ strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+string-template@~0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
+
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -7989,14 +8261,7 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-string-width@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e"
- dependencies:
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^3.0.0"
-
-string-width@^2.1.0, string-width@^2.1.1:
+string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
dependencies:
@@ -8007,7 +8272,7 @@ string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-string_decoder@~1.0.0, string_decoder@~1.0.3:
+string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies:
@@ -8017,7 +8282,7 @@ stringstream@~0.0.4, stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
-strip-ansi@3.0.1, strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies:
@@ -8029,6 +8294,17 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
+strip-ansi@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
+
+strip-bom-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca"
+ dependencies:
+ first-chunk-stream "^2.0.0"
+ strip-bom "^2.0.0"
+
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -8053,18 +8329,12 @@ strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
-style-loader@^0.20.2:
- version "0.20.2"
- resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.2.tgz#851b373c187890331776e9cde359eea9c95ecd00"
+style-loader@^0.21.0:
+ version "0.21.0"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.21.0.tgz#68c52e5eb2afc9ca92b6274be277ee59aea3a852"
dependencies:
loader-utils "^1.1.0"
- schema-utils "^0.4.3"
-
-subarg@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
- dependencies:
- minimist "^1.1.0"
+ schema-utils "^0.4.5"
supports-color@^2.0.0:
version "2.0.0"
@@ -8076,18 +8346,6 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
dependencies:
has-flag "^1.0.0"
-supports-color@^4.0.0, supports-color@^4.4.0:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
- dependencies:
- has-flag "^2.0.0"
-
-supports-color@^4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
- dependencies:
- has-flag "^2.0.0"
-
supports-color@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5"
@@ -8100,6 +8358,12 @@ supports-color@^5.2.0:
dependencies:
has-flag "^3.0.0"
+supports-color@^5.3.0, supports-color@^5.4.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
+ dependencies:
+ has-flag "^3.0.0"
+
svg4everybody@2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d"
@@ -8116,11 +8380,13 @@ svgo@^0.7.0:
sax "~1.2.1"
whet.extend "~0.9.9"
-syntax-error@^1.1.1:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c"
- dependencies:
- acorn-node "^1.2.0"
+symbol-observable@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
+
+symbol-observable@^0.2.2:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
table@^3.7.8:
version "3.8.3"
@@ -8137,9 +8403,9 @@ tapable@^0.1.8:
version "0.1.10"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
-tapable@^0.2.7:
- version "0.2.8"
- resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+tapable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
tar-pack@^3.4.0:
version "3.4.1"
@@ -8162,26 +8428,37 @@ tar@^2.2.1:
fstream "^1.0.2"
inherits "2"
+temp@^0.8.1:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59"
+ dependencies:
+ os-tmpdir "^1.0.0"
+ rimraf "~2.2.6"
+
term-size@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
dependencies:
execa "^0.7.0"
-test-exclude@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
+test-exclude@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.1.tgz#dfa222f03480bca69207ca728b37d74b45f724fa"
dependencies:
arrify "^1.0.1"
- micromatch "^2.3.11"
+ micromatch "^3.1.8"
object-assign "^4.1.0"
read-pkg-up "^1.0.1"
require-main-filename "^1.0.1"
-text-table@0.2.0, text-table@~0.2.0:
+text-table@^0.2.0, text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+textextensions@2:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
+
three-orbit-controls@^82.1.0:
version "82.1.0"
resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4"
@@ -8194,14 +8471,14 @@ three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
-through2@^2.0.0:
+through2@^2.0.0, through2@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
dependencies:
readable-stream "^2.1.5"
xtend "~4.0.1"
-through@2, "through@>=2.2.7 <3", through@^2.3.6, through@~2.3, through@~2.3.1:
+through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -8213,21 +8490,17 @@ thunky@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
-time-stamp@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
-
timeago.js@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-3.0.2.tgz#32a67e7c0d887ea42ca588d3aae26f77de5e76cc"
dependencies:
"@types/jquery" "^2.0.40"
-timed-out@^4.0.0:
+timed-out@^4.0.0, timed-out@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
-timers-browserify@^1.0.1, timers-browserify@^1.4.2:
+timers-browserify@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d"
dependencies:
@@ -8290,6 +8563,15 @@ to-regex@^3.0.1:
extend-shallow "^2.0.1"
regex-not "^1.0.0"
+to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -8330,10 +8612,6 @@ tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
-tty-browserify@~0.0.0:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
-
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -8361,11 +8639,18 @@ type-is@~1.6.15:
media-typer "0.3.0"
mime-types "~2.1.18"
-typedarray@^0.0.6, typedarray@~0.0.5:
+typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-uglify-js@^2.6, uglify-js@^2.8.29:
+uglify-es@^3.3.4:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+uglify-js@^2.6:
version "2.8.29"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
dependencies:
@@ -8378,13 +8663,18 @@ uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
-uglifyjs-webpack-plugin@^0.4.6:
- version "0.4.6"
- resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
+uglifyjs-webpack-plugin@^1.2.4:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641"
dependencies:
- source-map "^0.5.6"
- uglify-js "^2.8.29"
- webpack-sources "^1.0.1"
+ cacache "^10.0.4"
+ find-cache-dir "^1.0.0"
+ schema-utils "^0.4.5"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ uglify-es "^3.3.4"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
uid-number@^0.0.6:
version "0.0.6"
@@ -8394,23 +8684,23 @@ ultron@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
-umd@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e"
-
unc-path-regex@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
-undefsafe@^2.0.1:
+undefsafe@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76"
dependencies:
debug "^2.2.0"
-underscore@^1.8.3:
- version "1.8.3"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+underscore@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4"
+
+underscore@~1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
underscore@~1.7.0:
version "1.7.0"
@@ -8468,18 +8758,17 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
+untildify@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1"
+
unzip-response@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
upath@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.2.tgz#80aaae5395abc5fd402933ae2f58694f0860204c"
- dependencies:
- lodash.endswith "^4.2.1"
- lodash.isfunction "^3.0.8"
- lodash.isstring "^4.0.1"
- lodash.startswith "^4.2.1"
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.5.tgz#02cab9ecebe95bbec6d5fc2566325725ab6d1a73"
update-notifier@^2.3.0:
version "2.3.0"
@@ -8499,13 +8788,21 @@ urix@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
-url-loader@^0.6.2:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.6.2.tgz#a007a7109620e9d988d14bce677a1decb9a993f7"
+url-join@^2.0.2:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
+
+url-join@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a"
+
+url-loader@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.0.1.tgz#61bc53f1f184d7343da2728a1289ef8722ea45ee"
dependencies:
- loader-utils "^1.0.2"
- mime "^1.4.1"
- schema-utils "^0.3.0"
+ loader-utils "^1.1.0"
+ mime "^2.0.3"
+ schema-utils "^0.4.3"
url-parse-lax@^1.0.0:
version "1.0.0"
@@ -8513,6 +8810,12 @@ url-parse-lax@^1.0.0:
dependencies:
prepend-http "^1.0.1"
+url-parse-lax@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
+ dependencies:
+ prepend-http "^2.0.0"
+
url-parse@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
@@ -8531,7 +8834,7 @@ 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"
-url@^0.11.0, url@~0.11.0:
+url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
dependencies:
@@ -8552,18 +8855,18 @@ user-home@^2.0.0:
dependencies:
os-homedir "^1.0.0"
-useragent@^2.1.12:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
+useragent@2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e"
dependencies:
- lru-cache "4.1.x"
+ lru-cache "2.2.x"
tmp "0.0.x"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-util@0.10.3, util@^0.10.3, util@~0.10.1:
+util@0.10.3, util@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
dependencies:
@@ -8581,6 +8884,10 @@ uws@~9.14.0:
version "9.14.0"
resolved "https://registry.yarnpkg.com/uws/-/uws-9.14.0.tgz#fac8386befc33a7a3705cbd58dc47b430ca4dd95"
+v8-compile-cache@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz#8d32e4f16974654657e676e0e467a348e89b0dc4"
+
validate-npm-package-license@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
@@ -8608,11 +8915,41 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
+vinyl-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.3.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+ strip-bom-stream "^2.0.0"
+ vinyl "^1.1.0"
+
+vinyl@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
+ dependencies:
+ clone "^1.0.0"
+ clone-stats "^0.0.1"
+ replace-ext "0.0.1"
+
+vinyl@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c"
+ dependencies:
+ clone "^2.1.1"
+ clone-buffer "^1.0.0"
+ clone-stats "^1.0.0"
+ cloneable-readable "^1.0.0"
+ remove-trailing-separator "^1.0.1"
+ replace-ext "^1.0.0"
+
visibilityjs@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63"
-vm-browserify@0.0.4, vm-browserify@~0.0.1:
+vm-browserify@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
dependencies:
@@ -8633,48 +8970,40 @@ vue-eslint-parser@^2.0.1:
esquery "^1.0.0"
lodash "^4.17.4"
-vue-hot-reload-api@^2.2.0:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.2.4.tgz#683bd1d026c0d3b3c937d5875679e9a87ec6cd8f"
+vue-hot-reload-api@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"
-vue-loader@^14.1.1:
- version "14.1.1"
- resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-14.1.1.tgz#331f197fcea790d6b8662c29b850806e7eb29342"
+vue-loader@^15.2.0:
+ version "15.2.0"
+ resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.2.0.tgz#5a8138e490a1040942d2f10ae68fa72b5a923364"
dependencies:
- consolidate "^0.14.0"
+ "@vue/component-compiler-utils" "^1.2.1"
hash-sum "^1.0.2"
loader-utils "^1.1.0"
- lru-cache "^4.1.1"
- postcss "^6.0.8"
- postcss-load-config "^1.1.0"
- postcss-selector-parser "^2.0.0"
- prettier "^1.7.0"
- resolve "^1.4.0"
- source-map "^0.6.1"
- vue-hot-reload-api "^2.2.0"
- vue-style-loader "^4.0.1"
- vue-template-es2015-compiler "^1.6.0"
+ vue-hot-reload-api "^2.3.0"
+ vue-style-loader "^4.1.0"
-vue-resource@^1.3.5:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.3.5.tgz#021d8713e9d86a77e83169dfdd8eab6047369a71"
+vue-resource@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.5.0.tgz#ba0c6ef7af2eeace03cf24a91f529471be974c72"
dependencies:
- got "^7.1.0"
+ got "^8.0.3"
vue-router@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
-vue-style-loader@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.0.2.tgz#e89aa4702a0c6b9630d8de70b1cbddb06b9ad254"
+vue-style-loader@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.0.tgz#7588bd778e2c9f8d87bfc3c5a4a039638da7a863"
dependencies:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.5.13:
- version "2.5.13"
- resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.13.tgz#12a2aa0ecd6158ac5e5f14d294b0993f399c3d38"
+vue-template-compiler@^2.5.16:
+ version "2.5.16"
+ resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
@@ -8683,21 +9012,25 @@ vue-template-es2015-compiler@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18"
-vue@^2.5.13:
- version "2.5.13"
- resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.13.tgz#95bd31e20efcf7a7f39239c9aa6787ce8cf578e1"
+vue-virtual-scroll-list@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a"
+
+vue@^2.5.16:
+ version "2.5.16"
+ resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.16.tgz#07edb75e8412aaeed871ebafa99f4672584a0085"
vuex@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2"
-watchpack@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
+watchpack@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.5.0.tgz#231e783af830a22f8966f65c4c4bacc814072eed"
dependencies:
- async "^2.1.2"
- chokidar "^1.7.0"
+ chokidar "^2.0.2"
graceful-fs "^4.1.2"
+ neo-async "^2.5.0"
wbuf@^1.1.0, wbuf@^1.7.2:
version "1.7.2"
@@ -8705,9 +9038,15 @@ wbuf@^1.1.0, wbuf@^1.7.2:
dependencies:
minimalistic-assert "^1.0.0"
-webpack-bundle-analyzer@^2.10.0:
- version "2.10.0"
- resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.10.0.tgz#d0646cda342939f6f05eb632a090abbd90317446"
+webpack-addons@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/webpack-addons/-/webpack-addons-1.1.5.tgz#2b178dfe873fb6e75e40a819fa5c26e4a9bc837a"
+ dependencies:
+ jscodeshift "^0.4.0"
+
+webpack-bundle-analyzer@^2.11.1:
+ version "2.11.1"
+ resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9"
dependencies:
acorn "^5.3.0"
bfj-node4 "^5.2.0"
@@ -8722,19 +9061,64 @@ webpack-bundle-analyzer@^2.10.0:
opener "^1.4.3"
ws "^4.0.0"
-webpack-dev-middleware@1.12.2, webpack-dev-middleware@^1.12.0:
- version "1.12.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e"
+webpack-cli@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-2.1.2.tgz#9c9a4b90584f7b8acaf591238ef0667e04c817f6"
+ dependencies:
+ chalk "^2.3.2"
+ cross-spawn "^6.0.5"
+ diff "^3.5.0"
+ enhanced-resolve "^4.0.0"
+ envinfo "^4.4.2"
+ glob-all "^3.1.0"
+ global-modules "^1.0.0"
+ got "^8.2.0"
+ import-local "^1.0.0"
+ inquirer "^5.1.0"
+ interpret "^1.0.4"
+ jscodeshift "^0.5.0"
+ listr "^0.13.0"
+ loader-utils "^1.1.0"
+ lodash "^4.17.5"
+ log-symbols "^2.2.0"
+ mkdirp "^0.5.1"
+ p-each-series "^1.0.0"
+ p-lazy "^1.0.0"
+ prettier "^1.5.3"
+ supports-color "^5.3.0"
+ v8-compile-cache "^1.1.2"
+ webpack-addons "^1.1.5"
+ yargs "^11.1.0"
+ yeoman-environment "^2.0.0"
+ yeoman-generator "^2.0.4"
+
+webpack-dev-middleware@3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.1.3.tgz#8b32aa43da9ae79368c1bf1183f2b6cf5e1f39ed"
+ dependencies:
+ loud-rejection "^1.6.0"
+ memory-fs "~0.4.1"
+ mime "^2.1.0"
+ path-is-absolute "^1.0.0"
+ range-parser "^1.0.3"
+ url-join "^4.0.0"
+ webpack-log "^1.0.1"
+
+webpack-dev-middleware@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-2.0.6.tgz#a51692801e8310844ef3e3790e1eacfe52326fd4"
dependencies:
+ loud-rejection "^1.6.0"
memory-fs "~0.4.1"
- mime "^1.5.0"
+ mime "^2.1.0"
path-is-absolute "^1.0.0"
range-parser "^1.0.3"
- time-stamp "^2.0.0"
+ url-join "^2.0.2"
+ webpack-log "^1.0.1"
-webpack-dev-server@^2.11.2:
- version "2.11.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f"
+webpack-dev-server@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.4.tgz#9a08d13c4addd1e3b6d8ace116e86715094ad5b4"
dependencies:
ansi-html "0.0.7"
array-includes "^3.0.3"
@@ -8746,7 +9130,7 @@ webpack-dev-server@^2.11.2:
del "^3.0.0"
express "^4.16.2"
html-entities "^1.2.0"
- http-proxy-middleware "~0.17.4"
+ http-proxy-middleware "~0.18.0"
import-local "^1.0.0"
internal-ip "1.2.0"
ip "^1.1.5"
@@ -8761,8 +9145,27 @@ webpack-dev-server@^2.11.2:
spdy "^3.4.1"
strip-ansi "^3.0.0"
supports-color "^5.1.0"
- webpack-dev-middleware "1.12.2"
- yargs "6.6.0"
+ webpack-dev-middleware "3.1.3"
+ webpack-log "^1.1.2"
+ yargs "11.0.0"
+
+webpack-log@^1.0.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.2.0.tgz#a4b34cda6b22b518dbb0ab32e567962d5c72a43d"
+ dependencies:
+ chalk "^2.1.0"
+ log-symbols "^2.1.0"
+ loglevelnext "^1.0.1"
+ uuid "^3.1.0"
+
+webpack-log@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.1.2.tgz#cdc76016537eed24708dc6aa3d1e52189efee107"
+ dependencies:
+ chalk "^2.1.0"
+ log-symbols "^2.1.0"
+ loglevelnext "^1.0.1"
+ uuid "^3.1.0"
webpack-sources@^1.0.1:
version "1.0.1"
@@ -8771,36 +9174,40 @@ webpack-sources@^1.0.1:
source-list-map "^2.0.0"
source-map "~0.5.3"
-webpack-stats-plugin@^0.1.5:
- version "0.1.5"
- resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.1.5.tgz#29e5f12ebfd53158d31d656a113ac1f7b86179d9"
+webpack-sources@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack-stats-plugin@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595"
-webpack@^3.11.0:
- version "3.11.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894"
+webpack@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.7.0.tgz#a04f68dab86d5545fd0277d07ffc44e4078154c9"
dependencies:
acorn "^5.0.0"
- acorn-dynamic-import "^2.0.0"
+ acorn-dynamic-import "^3.0.0"
ajv "^6.1.0"
ajv-keywords "^3.1.0"
- async "^2.1.2"
- enhanced-resolve "^3.4.0"
- escope "^3.6.0"
- interpret "^1.0.0"
- json-loader "^0.5.4"
- json5 "^0.5.1"
+ chrome-trace-event "^0.1.1"
+ enhanced-resolve "^4.0.0"
+ eslint-scope "^3.7.1"
loader-runner "^2.3.0"
loader-utils "^1.1.0"
memory-fs "~0.4.1"
+ micromatch "^3.1.8"
mkdirp "~0.5.0"
+ neo-async "^2.5.0"
node-libs-browser "^2.0.0"
- source-map "^0.5.3"
- supports-color "^4.2.1"
- tapable "^0.2.7"
- uglifyjs-webpack-plugin "^0.4.6"
- watchpack "^1.4.0"
+ schema-utils "^0.4.4"
+ tapable "^1.0.0"
+ uglifyjs-webpack-plugin "^1.2.4"
+ watchpack "^1.5.0"
webpack-sources "^1.0.1"
- yargs "^8.0.2"
websocket-driver@>=0.5.1:
version "0.6.5"
@@ -8820,21 +9227,11 @@ whet.extend@~0.9.9:
version "0.9.9"
resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
-which-module@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
-
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-which@^1.1.1, which@^1.2.1, which@^1.2.9:
- version "1.2.12"
- resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
- dependencies:
- isexe "^1.1.1"
-
-which@^1.2.14:
+which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.9:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies:
@@ -8868,12 +9265,19 @@ wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-worker-loader@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.0.tgz#8cf21869a07add84d66f821d948d23c1eb98e809"
+worker-farm@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae"
+ dependencies:
+ errno "^0.1.4"
+ xtend "^4.0.1"
+
+worker-loader@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-1.1.1.tgz#920d74ddac6816fc635392653ed8b4af1929fd92"
dependencies:
loader-utils "^1.0.0"
- schema-utils "^0.3.0"
+ schema-utils "^0.4.0"
wrap-ansi@^2.0.0:
version "2.1.0"
@@ -8886,6 +9290,14 @@ wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+write-file-atomic@^1.2.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ slide "^1.1.5"
+
write-file-atomic@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
@@ -8928,7 +9340,7 @@ xregexp@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
-xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
@@ -8944,53 +9356,51 @@ yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
-yargs-parser@^4.2.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"
- dependencies:
- camelcase "^3.0.0"
-
-yargs-parser@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+yargs-parser@^9.0.2:
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077"
dependencies:
camelcase "^4.1.0"
-yargs@6.6.0:
- version "6.6.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
+yargs@11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.0.0.tgz#c052931006c5eee74610e5fc0354bedfd08a201b"
dependencies:
- camelcase "^3.0.0"
- cliui "^3.2.0"
+ cliui "^4.0.0"
decamelize "^1.1.1"
+ find-up "^2.1.0"
get-caller-file "^1.0.1"
- os-locale "^1.4.0"
- read-pkg-up "^1.0.1"
+ os-locale "^2.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
- string-width "^1.0.2"
- which-module "^1.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
y18n "^3.2.1"
- yargs-parser "^4.2.0"
+ yargs-parser "^9.0.2"
-yargs@^8.0.2:
- version "8.0.2"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+yargs@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77"
dependencies:
- camelcase "^4.1.0"
- cliui "^3.2.0"
+ cliui "^4.0.0"
decamelize "^1.1.1"
+ find-up "^2.1.0"
get-caller-file "^1.0.1"
os-locale "^2.0.0"
- read-pkg-up "^2.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1"
- yargs-parser "^7.0.0"
+ yargs-parser "^9.0.2"
+
+yargs@~1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b"
+ dependencies:
+ minimist "^0.1.0"
yargs@~3.10.0:
version "3.10.0"
@@ -9004,3 +9414,69 @@ yargs@~3.10.0:
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
+
+yeoman-environment@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.5.tgz#84f22bafa84088971fe99ea85f654a3a3dd2b693"
+ dependencies:
+ chalk "^2.1.0"
+ debug "^3.1.0"
+ diff "^3.3.1"
+ escape-string-regexp "^1.0.2"
+ globby "^6.1.0"
+ grouped-queue "^0.3.3"
+ inquirer "^3.3.0"
+ is-scoped "^1.0.0"
+ lodash "^4.17.4"
+ log-symbols "^2.1.0"
+ mem-fs "^1.1.0"
+ text-table "^0.2.0"
+ untildify "^3.0.2"
+
+yeoman-environment@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.6.tgz#ae1b21d826b363f3d637f88a7fc9ea7414cb5377"
+ dependencies:
+ chalk "^2.1.0"
+ debug "^3.1.0"
+ diff "^3.3.1"
+ escape-string-regexp "^1.0.2"
+ globby "^6.1.0"
+ grouped-queue "^0.3.3"
+ inquirer "^3.3.0"
+ is-scoped "^1.0.0"
+ lodash "^4.17.4"
+ log-symbols "^2.1.0"
+ mem-fs "^1.1.0"
+ text-table "^0.2.0"
+ untildify "^3.0.2"
+
+yeoman-generator@^2.0.4:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-2.0.5.tgz#57b0b3474701293cc9ec965288f3400b00887c81"
+ dependencies:
+ async "^2.6.0"
+ chalk "^2.3.0"
+ cli-table "^0.3.1"
+ cross-spawn "^6.0.5"
+ dargs "^5.1.0"
+ dateformat "^3.0.3"
+ debug "^3.1.0"
+ detect-conflict "^1.0.0"
+ error "^7.0.2"
+ find-up "^2.1.0"
+ github-username "^4.0.0"
+ istextorbinary "^2.2.1"
+ lodash "^4.17.10"
+ make-dir "^1.1.0"
+ mem-fs-editor "^4.0.0"
+ minimist "^1.2.0"
+ pretty-bytes "^4.0.2"
+ read-chunk "^2.1.0"
+ read-pkg-up "^3.0.0"
+ rimraf "^2.6.2"
+ run-async "^2.0.0"
+ shelljs "^0.8.0"
+ text-table "^0.2.0"
+ through2 "^2.0.0"
+ yeoman-environment "^2.0.5"