summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc3
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--CHANGELOG.md130
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock27
-rw-r--r--README.md2
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/admin.js4
-rw-r--r--app/assets/javascripts/api.js10
-rw-r--r--app/assets/javascripts/aside.js4
-rw-r--r--app/assets/javascripts/autosave.js4
-rw-r--r--app/assets/javascripts/awards_handler.js14
-rw-r--r--app/assets/javascripts/behaviors/autosize.js3
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js3
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js3
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js3
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es64
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js4
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js4
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js4
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es63
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es6164
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js3
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js6
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es66
-rw-r--r--app/assets/javascripts/boards/components/board.js.es66
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es64
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es64
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es624
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es66
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es610
-rw-r--r--app/assets/javascripts/boards/models/list.js.es616
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es64
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es620
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js231
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es64
-rw-r--r--app/assets/javascripts/breakpoints.js4
-rw-r--r--app/assets/javascripts/broadcast_message.js3
-rw-r--r--app/assets/javascripts/build.js12
-rw-r--r--app/assets/javascripts/build_artifacts.js4
-rw-r--r--app/assets/javascripts/build_variables.js.es66
-rw-r--r--app/assets/javascripts/commit.js4
-rw-r--r--app/assets/javascripts/commit/file.js4
-rw-r--r--app/assets/javascripts/commit/image_file.js4
-rw-r--r--app/assets/javascripts/commits.js4
-rw-r--r--app/assets/javascripts/compare.js4
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es65
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js4
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es64
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es626
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es62
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es68
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es64
-rw-r--r--app/assets/javascripts/dispatcher.js.es67
-rw-r--r--app/assets/javascripts/droplab/droplab.js126
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js14
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js3
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js18
-rw-r--r--app/assets/javascripts/dropzone_input.js6
-rw-r--r--app/assets/javascripts/due_date_select.js.es66
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es656
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es61
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es663
-rw-r--r--app/assets/javascripts/extensions/array.js.es66
-rw-r--r--app/assets/javascripts/extensions/custom_event.js.es612
-rw-r--r--app/assets/javascripts/extensions/element.js.es66
-rw-r--r--app/assets/javascripts/extensions/jquery.js3
-rw-r--r--app/assets/javascripts/files_comment_button.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es65
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es653
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es612
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es658
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es664
-rw-r--r--app/assets/javascripts/flash.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es610
-rw-r--r--app/assets/javascripts/gl_dropdown.js28
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es63
-rw-r--r--app/assets/javascripts/gl_form.js4
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js4
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js4
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js8
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js5
-rw-r--r--app/assets/javascripts/group_avatar.js8
-rw-r--r--app/assets/javascripts/group_label_subscription.js.es63
-rw-r--r--app/assets/javascripts/groups_select.js4
-rw-r--r--app/assets/javascripts/header.js4
-rw-r--r--app/assets/javascripts/importer_status.js3
-rw-r--r--app/assets/javascripts/issuable.js.es66
-rw-r--r--app/assets/javascripts/issuable/issuable_bundle.js.es61
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es642
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es669
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es613
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/help_state.js.es624
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es611
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es613
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6118
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es661
-rw-r--r--app/assets/javascripts/issuable_context.js4
-rw-r--r--app/assets/javascripts/issuable_form.js8
-rw-r--r--app/assets/javascripts/issue.js6
-rw-r--r--app/assets/javascripts/issue_status_select.js4
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es66
-rw-r--r--app/assets/javascripts/label_manager.js.es64
-rw-r--r--app/assets/javascripts/labels.js6
-rw-r--r--app/assets/javascripts/labels_select.js5
-rw-r--r--app/assets/javascripts/layout_nav.js12
-rw-r--r--app/assets/javascripts/lib/utils/animate.js3
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es616
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js4
-rw-r--r--app/assets/javascripts/lib/utils/notify.js3
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js.es626
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js8
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js3
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js7
-rw-r--r--app/assets/javascripts/lib/vue_resource.js.es62
-rw-r--r--app/assets/javascripts/line_highlighter.js14
-rw-r--r--app/assets/javascripts/logo.js3
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es610
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es64
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es64
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es65
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es640
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es612
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es63
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es63
-rw-r--r--app/assets/javascripts/merge_request.js6
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es618
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js.es645
-rw-r--r--app/assets/javascripts/merged_buttons.js6
-rw-r--r--app/assets/javascripts/milestone.js4
-rw-r--r--app/assets/javascripts/milestone_select.js4
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es621
-rw-r--r--app/assets/javascripts/namespace_select.js7
-rw-r--r--app/assets/javascripts/network/branch_graph.js20
-rw-r--r--app/assets/javascripts/network/network.js4
-rw-r--r--app/assets/javascripts/network/network_bundle.js3
-rw-r--r--app/assets/javascripts/new_branch_form.js8
-rw-r--r--app/assets/javascripts/new_commit_form.js6
-rw-r--r--app/assets/javascripts/notes.js33
-rw-r--r--app/assets/javascripts/notifications_dropdown.js4
-rw-r--r--app/assets/javascripts/notifications_form.js6
-rw-r--r--app/assets/javascripts/pipelines.js.es65
-rw-r--r--app/assets/javascripts/preview_markdown.js11
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es69
-rw-r--r--app/assets/javascripts/profile/profile.js.es64
-rw-r--r--app/assets/javascripts/project.js12
-rw-r--r--app/assets/javascripts/project_avatar.js4
-rw-r--r--app/assets/javascripts/project_find_file.js10
-rw-r--r--app/assets/javascripts/project_fork.js4
-rw-r--r--app/assets/javascripts/project_import.js4
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es67
-rw-r--r--app/assets/javascripts/project_new.js36
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/project_show.js5
-rw-r--r--app/assets/javascripts/projects_list.js3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es67
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js.es68
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es68
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es69
-rw-r--r--app/assets/javascripts/render_gfm.js3
-rw-r--r--app/assets/javascripts/render_math.js3
-rw-r--r--app/assets/javascripts/right_sidebar.js8
-rw-r--r--app/assets/javascripts/search.js4
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es623
-rw-r--r--app/assets/javascripts/shortcuts.js8
-rw-r--r--app/assets/javascripts/shortcuts_blob.js4
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js4
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js4
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js4
-rw-r--r--app/assets/javascripts/shortcuts_network.js4
-rw-r--r--app/assets/javascripts/sidebar.js.es63
-rw-r--r--app/assets/javascripts/single_file_diff.js6
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js5
-rw-r--r--app/assets/javascripts/snippets_list.js.es64
-rw-r--r--app/assets/javascripts/star.js4
-rw-r--r--app/assets/javascripts/subbable_resource.js.es63
-rw-r--r--app/assets/javascripts/subscription_select.js4
-rw-r--r--app/assets/javascripts/syntax_highlight.js4
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es68
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es64
-rw-r--r--app/assets/javascripts/todos.js.es65
-rw-r--r--app/assets/javascripts/tree.js4
-rw-r--r--app/assets/javascripts/u2f/authenticate.js.es68
-rw-r--r--app/assets/javascripts/u2f/error.js22
-rw-r--r--app/assets/javascripts/u2f/register.js8
-rw-r--r--app/assets/javascripts/u2f/util.js4
-rw-r--r--app/assets/javascripts/user.js.es64
-rw-r--r--app/assets/javascripts/user_tabs.js.es66
-rw-r--r--app/assets/javascripts/users/calendar.js14
-rw-r--r--app/assets/javascripts/users_select.js12
-rw-r--r--app/assets/javascripts/version_check_image.js.es610
-rw-r--r--app/assets/javascripts/visibility_select.js.es627
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es647
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es660
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es618
-rw-r--r--app/assets/javascripts/zen_mode.js4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss12
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss28
-rw-r--r--app/assets/stylesheets/framework/icons.scss1
-rw-r--r--app/assets/stylesheets/framework/page-header.scss4
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss103
-rw-r--r--app/assets/stylesheets/pages/notes.scss5
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss14
-rw-r--r--app/assets/stylesheets/pages/projects.scss56
-rw-r--r--app/assets/stylesheets/pages/search.scss1
-rw-r--r--app/assets/stylesheets/pages/status.scss3
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb4
-rw-r--r--app/controllers/confirmations_controller.rb8
-rw-r--r--app/controllers/dashboard/milestones_controller.rb4
-rw-r--r--app/controllers/projects/builds_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb5
-rw-r--r--app/controllers/projects/compare_controller.rb11
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb32
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb54
-rw-r--r--app/controllers/projects/hooks_controller.rb14
-rw-r--r--app/controllers/projects/issues_controller.rb12
-rw-r--r--app/controllers/projects/services_controller.rb4
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb18
-rw-r--r--app/controllers/search_controller.rb14
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb9
-rw-r--r--app/helpers/projects_helper.rb11
-rw-r--r--app/helpers/services_helper.rb20
-rw-r--r--app/helpers/todos_helper.rb3
-rw-r--r--app/helpers/version_check_helper.rb3
-rw-r--r--app/mailers/emails/notes.rb8
-rw-r--r--app/mailers/notify.rb20
-rw-r--r--app/models/ability.rb11
-rw-r--r--app/models/application_setting.rb84
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/ci/pipeline.rb15
-rw-r--r--app/models/ci/runner.rb33
-rw-r--r--app/models/ci/stage.rb15
-rw-r--r--app/models/commit.rb17
-rw-r--r--app/models/concerns/cache_markdown_field.rb7
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/mentionable.rb10
-rw-r--r--app/models/concerns/participable.rb7
-rw-r--r--app/models/concerns/presentable.rb7
-rw-r--r--app/models/concerns/taskable.rb2
-rw-r--r--app/models/concerns/time_trackable.rb72
-rw-r--r--app/models/cycle_analytics.rb60
-rw-r--r--app/models/cycle_analytics/summary.rb43
-rw-r--r--app/models/dashboard_milestone.rb5
-rw-r--r--app/models/generic_commit_status.rb10
-rw-r--r--app/models/key.rb7
-rw-r--r--app/models/merge_request.rb12
-rw-r--r--app/models/namespace.rb20
-rw-r--r--app/models/note.rb19
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_services/asana_service.rb4
-rw-r--r--app/models/project_services/assembla_service.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb6
-rw-r--r--app/models/project_services/bugzilla_service.rb2
-rw-r--r--app/models/project_services/buildkite_service.rb6
-rw-r--r--app/models/project_services/builds_email_service.rb4
-rw-r--r--app/models/project_services/campfire_service.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb16
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb4
-rw-r--r--app/models/project_services/ci_service.rb2
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/deployment_service.rb4
-rw-r--r--app/models/project_services/drone_ci_service.rb4
-rw-r--r--app/models/project_services/emails_on_push_service.rb4
-rw-r--r--app/models/project_services/external_wiki_service.rb6
-rw-r--r--app/models/project_services/flowdock_service.rb4
-rw-r--r--app/models/project_services/gemnasium_service.rb4
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/irker_service.rb4
-rw-r--r--app/models/project_services/issue_tracker_service.rb2
-rw-r--r--app/models/project_services/jira_service.rb4
-rw-r--r--app/models/project_services/kubernetes_service.rb2
-rw-r--r--app/models/project_services/mattermost_service.rb4
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb2
-rw-r--r--app/models/project_services/pipelines_email_service.rb4
-rw-r--r--app/models/project_services/pivotaltracker_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/project_services/redmine_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb2
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb2
-rw-r--r--app/models/project_services/teamcity_service.rb6
-rw-r--r--app/models/project_statistics.rb3
-rw-r--r--app/models/service.rb15
-rw-r--r--app/models/timelog.rb6
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/models/user.rb4
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/presenters/README.md154
-rw-r--r--app/presenters/ci/build_presenter.rb15
-rw-r--r--app/serializers/analytics_stage_entity.rb10
-rw-r--r--app/serializers/analytics_stage_serializer.rb3
-rw-r--r--app/serializers/analytics_summary_entity.rb7
-rw-r--r--app/serializers/analytics_summary_serializer.rb3
-rw-r--r--app/serializers/issuable_entity.rb4
-rw-r--r--app/serializers/pipeline_entity.rb10
-rw-r--r--app/services/ci/update_build_queue_service.rb19
-rw-r--r--app/services/issuable_base_service.rb16
-rw-r--r--app/services/merge_requests/base_service.rb16
-rw-r--r--app/services/merge_requests/refresh_service.rb41
-rw-r--r--app/services/notes/create_service.rb7
-rw-r--r--app/services/notes/post_process_service.rb3
-rw-r--r--app/services/notes/slash_commands_service.rb2
-rw-r--r--app/services/notification_service.rb22
-rw-r--r--app/services/slash_commands/interpret_service.rb44
-rw-r--r--app/services/system_note_service.rb57
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml3
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml10
-rw-r--r--app/views/notify/note_personal_snippet_email.html.haml1
-rw-r--r--app/views/notify/note_personal_snippet_email.text.erb8
-rw-r--r--app/views/projects/_visibility_select.html.haml7
-rw-r--r--app/views/projects/builds/show.html.haml6
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml8
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commit/pipelines.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml7
-rw-r--r--app/views/projects/edit.html.haml110
-rw-r--r--app/views/projects/hooks/_index.html.haml (renamed from app/views/projects/hooks/index.html.haml)0
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml1
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml1
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml3
-rw-r--r--app/views/projects/services/_index.html.haml (renamed from app/views/projects/services/index.html.haml)2
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml4
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml144
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml (renamed from app/views/projects/hooks/_project_hook.html.haml)0
-rw-r--r--app/views/projects/settings/integrations/show.html.haml3
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml2
-rw-r--r--app/views/shared/icons/_icon_stopwatch.svg1
-rw-r--r--app/views/shared/issuable/_form.html.haml3
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml20
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml14
-rw-r--r--app/views/shared/issuable/form/_title.html.haml4
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_issuables.html.haml3
-rw-r--r--app/views/shared/web_hooks/_form.html.haml3
-rw-r--r--app/workers/build_queue_worker.rb10
-rw-r--r--changelogs/unreleased/18786-go-to-a-project-order.yml4
-rw-r--r--changelogs/unreleased/19086-double-newline.yml4
-rw-r--r--changelogs/unreleased/19966-api-call-to-move-project-to-different-group-fails-when-using-group-and-project-names-instead-of-id.yml4
-rw-r--r--changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml4
-rw-r--r--changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml4
-rw-r--r--changelogs/unreleased/22111-remove-lock-icon-on-protected-tag.yml4
-rw-r--r--changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email4
-rw-r--r--changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml4
-rw-r--r--changelogs/unreleased/22974-trigger-service-events-through-api.yml4
-rw-r--r--changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml4
-rw-r--r--changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml4
-rw-r--r--changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml4
-rw-r--r--changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml4
-rw-r--r--changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml4
-rw-r--r--changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml4
-rw-r--r--changelogs/unreleased/24915_merge_slash_command.yml4
-rw-r--r--changelogs/unreleased/24923_nested_tasks.yml4
-rw-r--r--changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml4
-rw-r--r--changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml4
-rw-r--r--changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml4
-rw-r--r--changelogs/unreleased/25580-trucate-dropdown-for-long-branch.yml4
-rw-r--r--changelogs/unreleased/25678-remove-user-build.yml4
-rw-r--r--changelogs/unreleased/25701-standardize-text-colors.yml4
-rw-r--r--changelogs/unreleased/25705-your-commands-have-been-executed-is-overkill.yml3
-rw-r--r--changelogs/unreleased/25725-remove-window-object.yml4
-rw-r--r--changelogs/unreleased/25776-alerts-should-be-responsive.yml4
-rw-r--r--changelogs/unreleased/25829-update-username-button-remains-disabled-upon-failure.yml4
-rw-r--r--changelogs/unreleased/25898-ci-icon-color-mr.yml4
-rw-r--r--changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml4
-rw-r--r--changelogs/unreleased/25946-manual-pipeline-dropdown-casing.yml4
-rw-r--r--changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml5
-rw-r--r--changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml4
-rw-r--r--changelogs/unreleased/25996-Move-award-emoji-out-of-the-discussion-tab-for-MR.yml4
-rw-r--r--changelogs/unreleased/26014-fix-update-doc.yml4
-rw-r--r--changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml4
-rw-r--r--changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml4
-rw-r--r--changelogs/unreleased/26117-sort-pipeline-for-commit.yml4
-rw-r--r--changelogs/unreleased/26129-add-link-to-branches-page.yml4
-rw-r--r--changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml4
-rw-r--r--changelogs/unreleased/26155-merge-request-tabs-don-t-render-when-no-commits-available.yml4
-rw-r--r--changelogs/unreleased/26192-fixes-too-short-input.yml4
-rw-r--r--changelogs/unreleased/26207-add-hover-animations.yml4
-rw-r--r--changelogs/unreleased/26226-generate-all-haml-fixtures-within-teaspoon-fixtures-task.yml4
-rw-r--r--changelogs/unreleased/26238-buttons-not-accessible.yml4
-rw-r--r--changelogs/unreleased/26261-post-api-v3-projects-idorproject-commits-commits-does-not-work-with-project-path.yml4
-rw-r--r--changelogs/unreleased/26352-user-dropdown-settings.yml4
-rw-r--r--changelogs/unreleased/26435-show-project-avatars-on-mobile.yml4
-rw-r--r--changelogs/unreleased/26445-accessible-piplelines-buttons.yml4
-rw-r--r--changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml4
-rw-r--r--changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml5
-rw-r--r--changelogs/unreleased/26447-fix-tab-list-order.yml4
-rw-r--r--changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml4
-rw-r--r--changelogs/unreleased/26504-mr-discussion-btn.yml4
-rw-r--r--changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml4
-rw-r--r--changelogs/unreleased/26615-pipeline-status-cell.yml4
-rw-r--r--changelogs/unreleased/26616-fix-search-group-project-filters.yml4
-rw-r--r--changelogs/unreleased/26785-fix-droplab-in-ie-11-v1.yml4
-rw-r--r--changelogs/unreleased/26787-add-copy-icon-hover-state.yml4
-rw-r--r--changelogs/unreleased/27066-textarea-border.yml4
-rw-r--r--changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.yml4
-rw-r--r--changelogs/unreleased/8-15-stable.yml4
-rw-r--r--changelogs/unreleased/add-changelog-search-bar-first-iteration.yml4
-rw-r--r--changelogs/unreleased/add_email_password_confirmation.yml4
-rw-r--r--changelogs/unreleased/additional-award-emoji-repositioning-fixes.yml4
-rw-r--r--changelogs/unreleased/allow_plus_sign_for_snippets.yml4
-rw-r--r--changelogs/unreleased/asciidoctor-plantuml.yml4
-rw-r--r--changelogs/unreleased/badge-color-on-white-bg.yml4
-rw-r--r--changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml4
-rw-r--r--changelogs/unreleased/bug-project-feature-compatibility.yml5
-rw-r--r--changelogs/unreleased/clipboard-button-commit-sha.yml3
-rw-r--r--changelogs/unreleased/clipboard-button-text.yml3
-rw-r--r--changelogs/unreleased/didemacet-ci-lint-page.yml4
-rw-r--r--changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml4
-rw-r--r--changelogs/unreleased/dot-in-project-queries.yml4
-rw-r--r--changelogs/unreleased/dz-nested-group-misc.yml4
-rw-r--r--changelogs/unreleased/dz-rename-invalid-users.yml4
-rw-r--r--changelogs/unreleased/env-var-in-redis-config.yml4
-rw-r--r--changelogs/unreleased/feature-1376-allow-write-access-deploy-keys.yml4
-rw-r--r--changelogs/unreleased/feature-admin-merge-groups-and-projects.yml4
-rw-r--r--changelogs/unreleased/feature-log-ldap-to-application-log.yml4
-rw-r--r--changelogs/unreleased/feature-more-storage-statistics.yml4
-rw-r--r--changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml4
-rw-r--r--changelogs/unreleased/filename-to-file-path.yml3
-rw-r--r--changelogs/unreleased/fill-authorized-projects.yml4
-rw-r--r--changelogs/unreleased/fix-api-deprecation.yml4
-rw-r--r--changelogs/unreleased/fix-api-mr-permissions.yml4
-rw-r--r--changelogs/unreleased/fix-blame-500.yml4
-rw-r--r--changelogs/unreleased/fix-boards-search-typo.yml4
-rw-r--r--changelogs/unreleased/fix-broken-url-on-group-avatar.yml4
-rw-r--r--changelogs/unreleased/fix-build-sort-order.yml4
-rw-r--r--changelogs/unreleased/fix-copy-issues-empty-state.yml4
-rw-r--r--changelogs/unreleased/fix-guest-access-posting-to-notes.yml4
-rw-r--r--changelogs/unreleased/fix-keep-artifacts-button-visibility.yml4
-rw-r--r--changelogs/unreleased/fix-light-hr-in-descriptions.yml4
-rw-r--r--changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml4
-rw-r--r--changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml4
-rw-r--r--changelogs/unreleased/fix-project-delete-tooltip.yml4
-rw-r--r--changelogs/unreleased/fix-serialized-commit-path.yml4
-rw-r--r--changelogs/unreleased/fix-timezone-due-date-picker.yml4
-rw-r--r--changelogs/unreleased/fix-user-api-confirm-param.yml4
-rw-r--r--changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml4
-rw-r--r--changelogs/unreleased/fix_broken_diff_discussions.yml4
-rw-r--r--changelogs/unreleased/get_last_used_date_of_ssh_key.yml4
-rw-r--r--changelogs/unreleased/issue-boards-animate.yml4
-rw-r--r--changelogs/unreleased/issue-filter-click-to-search.yml4
-rw-r--r--changelogs/unreleased/issue_22664.yml4
-rw-r--r--changelogs/unreleased/issue_25017.yml4
-rw-r--r--changelogs/unreleased/issue_25578.yml4
-rw-r--r--changelogs/unreleased/issue_25682.yml4
-rw-r--r--changelogs/unreleased/issues-8081.yml4
-rw-r--r--changelogs/unreleased/ldap_maint_task.yml4
-rw-r--r--changelogs/unreleased/login-page-font-size.yml4
-rw-r--r--changelogs/unreleased/merge-dropdown-this-context.yml4
-rw-r--r--changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml4
-rw-r--r--changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml4
-rw-r--r--changelogs/unreleased/newline-eslint-rule.yml4
-rw-r--r--changelogs/unreleased/no_project_notes.yml4
-rw-r--r--changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml4
-rw-r--r--changelogs/unreleased/pc-add-gitaly-to-architecture.yml4
-rw-r--r--changelogs/unreleased/pipelines-graph-html-css.yml4
-rw-r--r--changelogs/unreleased/pmq20-gitlab-ce-psvr-head-cache.yml4
-rw-r--r--changelogs/unreleased/re-style-issue-new-branch.yml3
-rw-r--r--changelogs/unreleased/recaptcha_500.yml4
-rw-r--r--changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml5
-rw-r--r--changelogs/unreleased/reduce-queries-milestone-index.yml4
-rw-r--r--changelogs/unreleased/refresh-authorizations-tighter-lease.yml4
-rw-r--r--changelogs/unreleased/remove-project-authorizations-id-column.yml4
-rw-r--r--changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml4
-rw-r--r--changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml4
-rw-r--r--changelogs/unreleased/sandish-gitlab-ce-update_ret_val.yml4
-rw-r--r--changelogs/unreleased/single-edit-comment-widget-2.yml5
-rw-r--r--changelogs/unreleased/support-google-cloud-storage-backups.yml4
-rw-r--r--changelogs/unreleased/switch-to-sassc.yml4
-rw-r--r--changelogs/unreleased/update-gitlab-markup-gem.yml4
-rw-r--r--changelogs/unreleased/upgrade-omniauth.yml4
-rw-r--r--changelogs/unreleased/validate-title-length.yml4
-rw-r--r--changelogs/unreleased/view-ce-vs-ee.yml4
-rw-r--r--changelogs/unreleased/wip-mr-from-commits.yml4
-rw-r--r--changelogs/unreleased/zj-requeue-pending-delete.yml4
-rw-r--r--changelogs/unreleased/zj-unadressable-url-variables.yml4
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--config/initializers/metrics.rb219
-rw-r--r--config/routes/project.rb2
-rw-r--r--db/migrate/20161223034433_add_estimate_to_issuables_ce.rb25
-rw-r--r--db/migrate/20161223034646_create_timelogs_ce.rb24
-rw-r--r--db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb17
-rw-r--r--db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb17
-rw-r--r--db/post_migrate/20170104150317_requeue_pending_delete_projects.rb49
-rw-r--r--db/schema.rb18
-rw-r--r--doc/README.md10
-rw-r--r--doc/administration/monitoring/performance/introduction.md5
-rw-r--r--doc/administration/monitoring/performance/prometheus.md102
-rw-r--r--doc/administration/raketasks/maintenance.md1
-rw-r--r--doc/api/README.md8
-rw-r--r--doc/api/issues.md140
-rw-r--r--doc/api/merge_requests.md139
-rw-r--r--doc/ci/git_submodules.md13
-rw-r--r--doc/ci/yaml/README.md35
-rw-r--r--doc/development/code_review.md12
-rw-r--r--doc/development/frontend.md78
-rw-r--r--doc/development/merge_request_performance_guidelines.md8
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/integration/oauth_provider.md8
-rw-r--r--doc/integration/omniauth.md10
-rw-r--r--doc/profile/2fa_u2f_authenticate.pngbin17585 -> 0 bytes
-rw-r--r--doc/profile/two_factor_authentication.md144
-rw-r--r--doc/project_services/kubernetes.md2
-rw-r--r--doc/project_services/project_services.md1
-rw-r--r--doc/public_access/img/restrict_visibility_levels.pngbin0 -> 24593 bytes
-rw-r--r--doc/public_access/public_access.md9
-rw-r--r--doc/update/8.15-to-8.16.md2
-rw-r--r--doc/user/account/security.md4
-rw-r--r--doc/user/account/two_factor_authentication.md69
-rw-r--r--doc/user/markdown.md20
-rw-r--r--doc/user/permissions.md30
-rw-r--r--doc/user/profile/account/img/2fa.png (renamed from doc/profile/2fa.png)bin22047 -> 22047 bytes
-rw-r--r--doc/user/profile/account/img/2fa_auth.png (renamed from doc/profile/2fa_auth.png)bin14535 -> 14535 bytes
-rw-r--r--doc/user/profile/account/img/2fa_u2f_authenticate.pngbin0 -> 17582 bytes
-rw-r--r--doc/user/profile/account/img/2fa_u2f_register.png (renamed from doc/profile/2fa_u2f_register.png)bin35186 -> 35186 bytes
-rw-r--r--doc/user/profile/account/index.md5
-rw-r--r--doc/user/profile/account/two_factor_authentication.md215
-rw-r--r--doc/user/project/issues/confidential_issues.md68
-rw-r--r--doc/user/project/issues/due_dates.md37
-rw-r--r--doc/user/project/issues/img/confidential_issues_create.pngbin0 -> 9659 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_index_page.pngbin0 -> 9949 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_issue_page.pngbin0 -> 16089 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_search_guest.pngbin0 -> 10014 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_search_master.pngbin0 -> 15332 bytes
-rw-r--r--doc/user/project/issues/img/confidential_issues_system_notes.pngbin0 -> 3025 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_create.pngbin0 -> 7705 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_edit_sidebar.pngbin0 -> 2424 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_issues_index_page.pngbin0 -> 21402 bytes
-rw-r--r--doc/user/project/issues/img/due_dates_todos.pngbin0 -> 5644 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_conflict_editor.pngbin0 -> 50422 bytes
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md12
-rw-r--r--doc/user/project/slash_commands.md4
-rw-r--r--doc/workflow/README.md5
-rw-r--r--doc/workflow/importing/migrating_from_svn.md2
-rw-r--r--doc/workflow/time-tracking/time-tracking-example.pngbin0 -> 48350 bytes
-rw-r--r--doc/workflow/time-tracking/time-tracking-sidebar.pngbin0 -> 19467 bytes
-rw-r--r--doc/workflow/time_tracking.md73
-rw-r--r--features/project/active_tab.feature6
-rw-r--r--features/steps/dashboard/dashboard.rb6
-rw-r--r--features/steps/dashboard/issues.rb4
-rw-r--r--features/steps/dashboard/merge_requests.rb4
-rw-r--r--features/steps/group/milestones.rb2
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/profile/profile.rb2
-rw-r--r--features/steps/project/active_tab.rb8
-rw-r--r--features/steps/project/deploy_keys.rb4
-rw-r--r--features/steps/project/fork.rb4
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/hooks.rb6
-rw-r--r--features/steps/project/merge_requests/acceptance.rb2
-rw-r--r--features/steps/project/merge_requests/revert.rb2
-rw-r--r--features/steps/project/redirects.rb4
-rw-r--r--features/steps/project/services.rb2
-rw-r--r--features/steps/project/source/browse_files.rb2
-rw-r--r--features/steps/project/source/markdown_render.rb2
-rw-r--r--features/steps/project/team_management.rb2
-rw-r--r--features/steps/shared/admin.rb2
-rw-r--r--features/steps/shared/group.rb2
-rw-r--r--features/steps/shared/paths.rb2
-rw-r--r--features/steps/shared/project.rb18
-rw-r--r--features/steps/user.rb2
-rw-r--r--lib/api/branches.rb7
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/deploy_keys.rb27
-rw-r--r--lib/api/entities.rb7
-rw-r--r--lib/api/helpers.rb12
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/merge_request_diffs.rb8
-rw-r--r--lib/api/merge_requests.rb33
-rw-r--r--lib/api/notes.rb26
-rw-r--r--lib/api/services.rb48
-rw-r--r--lib/api/subscriptions.rb4
-rw-r--r--lib/api/time_tracking_endpoints.rb114
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb12
-rw-r--r--lib/banzai/filter/user_reference_filter.rb4
-rw-r--r--lib/banzai/renderer.rb4
-rw-r--r--lib/ci/api/builds.rb9
-rw-r--r--lib/gitlab/ci/status/build/factory.rb7
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb27
-rw-r--r--lib/gitlab/ci/status/external/common.rb22
-rw-r--r--lib/gitlab/ci/status/external/factory.rb13
-rw-r--r--lib/gitlab/ci/status/factory.rb43
-rw-r--r--lib/gitlab/ci/status/pipeline/factory.rb2
-rw-r--r--lib/gitlab/ci/status/pipeline/success_with_warnings.rb31
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb4
-rw-r--r--lib/gitlab/ci/status/success_warning.rb33
-rw-r--r--lib/gitlab/current_settings.rb44
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/base_event.rb)26
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb31
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb54
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/review_event.rb)8
-rw-r--r--lib/gitlab/cycle_analytics/code_stage.rb21
-rw-r--r--lib/gitlab/cycle_analytics/event_fetcher.rb9
-rw-r--r--lib/gitlab/cycle_analytics/events.rb38
-rw-r--r--lib/gitlab/cycle_analytics/events_query.rb37
-rw-r--r--lib/gitlab/cycle_analytics/issue_event.rb27
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/production_event.rb)5
-rw-r--r--lib/gitlab/cycle_analytics/issue_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/metrics_fetcher.rb60
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/plan_event.rb)10
-rw-r--r--lib/gitlab/cycle_analytics/plan_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/production_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb9
-rw-r--r--lib/gitlab/cycle_analytics/production_stage.rb28
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/code_event.rb)8
-rw-r--r--lib/gitlab/cycle_analytics/review_stage.rb21
-rw-r--r--lib/gitlab/cycle_analytics/stage.rb9
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb23
-rw-r--r--lib/gitlab/cycle_analytics/staging_event_fetcher.rb (renamed from lib/gitlab/cycle_analytics/staging_event.rb)9
-rw-r--r--lib/gitlab/cycle_analytics/staging_stage.rb22
-rw-r--r--lib/gitlab/cycle_analytics/summary/base.rb20
-rw-r--r--lib/gitlab/cycle_analytics/summary/commit.rb39
-rw-r--r--lib/gitlab/cycle_analytics/summary/deploy.rb11
-rw-r--r--lib/gitlab/cycle_analytics/summary/issue.rb21
-rw-r--r--lib/gitlab/cycle_analytics/test_event.rb13
-rw-r--r--lib/gitlab/cycle_analytics/test_event_fetcher.rb6
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb29
-rw-r--r--lib/gitlab/database/median.rb5
-rw-r--r--lib/gitlab/email/handler.rb3
-rw-r--r--lib/gitlab/email/handler/base_handler.rb43
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb1
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb7
-rw-r--r--lib/gitlab/email/handler/reply_processing.rb54
-rw-r--r--lib/gitlab/email/handler/unsubscribe_handler.rb32
-rw-r--r--lib/gitlab/github_import/project_creator.rb4
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/members_mapper.rb8
-rw-r--r--lib/gitlab/import_export/relation_factory.rb12
-rw-r--r--lib/gitlab/import_sources.rb2
-rw-r--r--lib/gitlab/incoming_email.rb9
-rw-r--r--lib/gitlab/project_search_results.rb28
-rw-r--r--lib/gitlab/search_results.rb4
-rw-r--r--lib/gitlab/time_tracking_formatter.rb34
-rw-r--r--lib/gitlab/user_access.rb4
-rw-r--r--lib/gitlab/view/presenter/base.rb28
-rw-r--r--lib/gitlab/view/presenter/delegated.rb19
-rw-r--r--lib/gitlab/view/presenter/factory.rb24
-rw-r--r--lib/gitlab/view/presenter/simple.rb17
-rw-r--r--lib/gitlab/workhorse.rb9
-rw-r--r--lib/mattermost/client.rb22
-rw-r--r--lib/mattermost/command.rb2
-rw-r--r--lib/mattermost/team.rb2
-rw-r--r--lib/tasks/gitlab/info.rake5
-rw-r--r--public/robots.txt5
-rw-r--r--spec/controllers/admin/services_controller_spec.rb26
-rw-r--r--spec/controllers/health_check_controller_spec.rb6
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb38
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb30
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb48
-rw-r--r--spec/controllers/projects/services_controller_spec.rb1
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb20
-rw-r--r--spec/factories/ci/stages.rb3
-rw-r--r--spec/factories/notes.rb6
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/factories/timelogs.rb9
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb3
-rw-r--r--spec/features/admin/admin_health_check_spec.rb9
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb3
-rw-r--r--spec/features/admin/admin_settings_spec.rb5
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb9
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb28
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb10
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb26
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb18
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb49
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb16
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb39
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb52
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb3
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb27
-rw-r--r--spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb41
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb60
-rw-r--r--spec/features/merge_requests/wip_message_spec.rb63
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb3
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb62
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb61
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb47
-rw-r--r--spec/features/search_spec.rb40
-rw-r--r--spec/features/security/project/internal_access_spec.rb4
-rw-r--r--spec/features/security/project/private_access_spec.rb4
-rw-r--r--spec/features/security/project/public_access_spec.rb4
-rw-r--r--spec/features/task_lists_spec.rb44
-rw-r--r--spec/features/todos/todos_spec.rb2
-rw-r--r--spec/finders/merge_requests_finder_spec.rb2
-rw-r--r--spec/finders/move_to_project_finder_spec.rb22
-rw-r--r--spec/initializers/metrics_spec.rb16
-rw-r--r--spec/javascripts/.eslintrc4
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es61
-rw-r--r--spec/javascripts/activities_spec.js.es618
-rw-r--r--spec/javascripts/awards_handler_spec.js3
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js3
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js3
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js3
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es610
-rw-r--r--spec/javascripts/dashboard_spec.js.es63
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es64
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es61
-rw-r--r--spec/javascripts/environments/environment_spec.js.es6127
-rw-r--r--spec/javascripts/environments/mock_data.js.es614
-rw-r--r--spec/javascripts/extensions/array_spec.js.es63
-rw-r--r--spec/javascripts/extensions/jquery_spec.js3
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js.es641
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es66
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js3
-rw-r--r--spec/javascripts/fixtures/environments/environments.html.haml2
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml11
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js.es665
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es633
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es66
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js173
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js299
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js8
-rw-r--r--spec/javascripts/header_spec.js4
-rw-r--r--spec/javascripts/helpers/class_spec_helper.js.es69
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js.es635
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js.es6201
-rw-r--r--spec/javascripts/issue_spec.js27
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es616
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es617
-rw-r--r--spec/javascripts/line_highlighter_spec.js23
-rw-r--r--spec/javascripts/merge_request_spec.js3
-rw-r--r--spec/javascripts/merge_request_widget_spec.js41
-rw-r--r--spec/javascripts/new_branch_spec.js3
-rw-r--r--spec/javascripts/notes_spec.js5
-rw-r--r--spec/javascripts/pretty_time_spec.js.es622
-rw-r--r--spec/javascripts/project_title_spec.js3
-rw-r--r--spec/javascripts/right_sidebar_spec.js5
-rw-r--r--spec/javascripts/search_autocomplete_spec.js3
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js3
-rw-r--r--spec/javascripts/subbable_resource_spec.js.es64
-rw-r--r--spec/javascripts/syntax_highlight_spec.js3
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js3
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js7
-rw-r--r--spec/javascripts/u2f/register_spec.js3
-rw-r--r--spec/javascripts/visibility_select_spec.js.es6100
-rw-r--r--spec/javascripts/zen_mode_spec.js3
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb125
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb110
-rw-r--r--spec/lib/gitlab/ci/status/external/common_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/status/external/factory_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/status/factory_spec.rb133
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/factory_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb69
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb75
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb68
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb137
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb (renamed from spec/lib/gitlab/cycle_analytics/plan_event_spec.rb)10
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_event_spec.rb11
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb30
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb (renamed from spec/models/cycle_analytics/summary_spec.rb)20
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb12
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_stage_spec.rb8
-rw-r--r--spec/lib/gitlab/email/email_shared_blocks.rb2
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb61
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb58
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml10
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb42
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb115
-rw-r--r--spec/lib/gitlab/user_access_spec.rb9
-rw-r--r--spec/lib/gitlab/view/presenter/base_spec.rb51
-rw-r--r--spec/lib/gitlab/view/presenter/delegated_spec.rb29
-rw-r--r--spec/lib/gitlab/view/presenter/factory_spec.rb38
-rw-r--r--spec/lib/gitlab/view/presenter/simple_spec.rb29
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb23
-rw-r--r--spec/models/ability_spec.rb27
-rw-r--r--spec/models/ci/build_spec.rb10
-rw-r--r--spec/models/ci/pipeline_spec.rb103
-rw-r--r--spec/models/ci/runner_spec.rb56
-rw-r--r--spec/models/ci/stage_spec.rb72
-rw-r--r--spec/models/commit_spec.rb46
-rw-r--r--spec/models/concerns/has_status_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb38
-rw-r--r--spec/models/concerns/mentionable_spec.rb24
-rw-r--r--spec/models/concerns/presentable_spec.rb15
-rw-r--r--spec/models/cycle_analytics/code_spec.rb22
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb4
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb4
-rw-r--r--spec/models/cycle_analytics/production_spec.rb6
-rw-r--r--spec/models/cycle_analytics/review_spec.rb4
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb6
-rw-r--r--spec/models/cycle_analytics/test_spec.rb10
-rw-r--r--spec/models/generic_commit_status_spec.rb33
-rw-r--r--spec/models/key_spec.rb27
-rw-r--r--spec/models/merge_request_spec.rb52
-rw-r--r--spec/models/namespace_spec.rb11
-rw-r--r--spec/models/note_spec.rb81
-rw-r--r--spec/models/project_spec.rb108
-rw-r--r--spec/models/project_statistics_spec.rb2
-rw-r--r--spec/models/timelog_spec.rb10
-rw-r--r--spec/models/user_spec.rb131
-rw-r--r--spec/policies/base_policy_spec.rb17
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb77
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/boards_spec.rb4
-rw-r--r--spec/requests/api/commit_statuses_spec.rb28
-rw-r--r--spec/requests/api/deploy_keys_spec.rb13
-rw-r--r--spec/requests/api/environments_spec.rb2
-rw-r--r--spec/requests/api/fork_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb16
-rw-r--r--spec/requests/api/helpers_spec.rb3
-rw-r--r--spec/requests/api/internal_spec.rb13
-rw-r--r--spec/requests/api/issues_spec.rb16
-rw-r--r--spec/requests/api/labels_spec.rb2
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb41
-rw-r--r--spec/requests/api/notes_spec.rb20
-rw-r--r--spec/requests/api/notification_settings_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb4
-rw-r--r--spec/requests/api/projects_spec.rb29
-rw-r--r--spec/requests/api/runners_spec.rb4
-rw-r--r--spec/requests/api/services_spec.rb11
-rw-r--r--spec/requests/api/todos_spec.rb19
-rw-r--r--spec/requests/api/triggers_spec.rb2
-rw-r--r--spec/requests/api/variables_spec.rb2
-rw-r--r--spec/requests/ci/api/builds_spec.rb40
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb35
-rw-r--r--spec/serializers/analytics_stage_serializer_spec.rb24
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb29
-rw-r--r--spec/serializers/pipeline_entity_spec.rb12
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb47
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb115
-rw-r--r--spec/services/notes/create_service_spec.rb51
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb12
-rw-r--r--spec/services/notification_service_spec.rb49
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb85
-rw-r--r--spec/services/system_note_service_spec.rb88
-rw-r--r--spec/services/todo_service_spec.rb16
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb132
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb13
-rw-r--r--spec/support/notify_shared_examples.rb17
-rw-r--r--spec/support/taskable_shared_examples.rb24
-rw-r--r--spec/support/test_env.rb3
-rw-r--r--spec/support/time_tracking_shared_examples.rb82
-rw-r--r--spec/teaspoon_env.rb2
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml76
884 files changed, 10711 insertions, 4144 deletions
diff --git a/.eslintrc b/.eslintrc
index e13f76b213c..9ab0145820d 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -15,6 +15,7 @@
"filenames"
],
"rules": {
- "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
+ "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
+ "no-multiple-empty-lines": ["error", { "max": 1 }]
}
}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 68690ff33da..d30deef0096 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -35,7 +35,6 @@ stages:
.dedicated-runner: &dedicated-runner
tags:
- gitlab-org
- - 2gb
.knapsack-state: &knapsack-state
services: []
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cabfef84b24..aecacbee2f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,136 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.16.0 (2017-02-22)
+
+- Add LDAP Rake task to rename a provider. !2181
+- Validate label's title length. !5767 (Tomáš Kukrál)
+- Allow to add deploy keys with write-access. !5807 (Ali Ibrahim)
+- Allow to use + symbol in filenames. !6644 (blackst0ne)
+- Search bar redesign first iteration. !7345
+- Fix date inconsistency on due date picker. !7422 (Giuliano Varriale)
+- Add email confirmation field to registration form. !7432
+- Updated project visibility settings UX. !7645
+- Go to a project order. !7737 (Jacopo Beschi @jacopo-beschi)
+- Support slash comand `/merge` for merging merge requests. !7746 (Jarka Kadlecova)
+- Add more storage statistics. !7754 (Markus Koller)
+- Add support for PlantUML diagrams in AsciiDoc documents. !7810 (Horacio Sanson)
+- Remove extra orphaned rows when removing stray namespaces. !7841
+- Added lighter count badge background-color for on white backgrounds. !7873
+- Fixes issue boards list colored top border visual glitch. !7898 (Pier Paolo Ramon)
+- change 'gray' color theme name to 'black' to match the actual color. !7908 (BM5k)
+- Remove trailing whitespace when generating changelog entry. !7948
+- Remove checking branches state in issue new branch button. !8023
+- Log LDAP blocking/unblocking events to application log. !8042 (Markus Koller)
+- ensure permalinks scroll to correct position on multiple clicks. !8046
+- Allow to use ENV variables in redis config. !8073 (Semyon Pupkov)
+- fix button layout issue on branches page. !8074
+- Reduce DB-load for build-queues by storing last_update in Redis. !8084
+- Record and show last used date of SSH Keys. !8113 (Vincent Wong)
+- Resolves overflow in compare branch and tags dropdown. !8118
+- Replace wording for slash command confirmation message. !8123
+- remove build_user. !8162 (Arsenev Vladislav)
+- Prevent empty pagination when list is not empty. !8172
+- Make successful pipeline emails off for watchers. !8176
+- Improve copy in Issue Tracker empty state. !8202
+- Adds CSS class to status icon on MR widget to prevent non-colored icon. !8219
+- Improve visibility of "Resolve conflicts" and "Merge locally" actions. !8229
+- Add Gitaly to the architecture documentation. !8264 (Pablo Carranza <pablo@gitlab.com>)
+- Sort numbers in build names more intelligently. !8277
+- Show nested groups tab on group page. !8308
+- Rename users with namespace ending with .git. !8309
+- Rename filename to file path in tooltip of file header in merge request diff. !8314
+- About GitLab link in sidebar that links to help page. !8316
+- Merged the 'Groups' and 'Projects' tabs when viewing user profiles. !8323 (James Gregory)
+- re-enable change username button after failure. !8332
+- Darkened hr border color in descriptions because of update of bootstrap. !8333
+- display merge request discussion tab for empty branches. !8347
+- Fix double spaced CI log. !8349 (Jared Deckard <jared.deckard@gmail.com>)
+- Refactored note edit form to improve frontend performance on MR and Issues pages, especially pages with has a lot of discussions in it. !8356
+- Make CTRL+Enter submits a new merge request. !8360 (Saad Shahd)
+- Fixes too short input for placeholder message in commit listing page. !8367
+- Fix typo: seach to search. !8370
+- Adds label to Environments "Date Created". !8376 (Saad Shahd)
+- Convert project setting text into protected branch path link. !8377 (Ken Ding)
+- Precompile all JavaScript fixtures. !8384
+- Use original casing for build action text. !8387
+- Scroll to bottom on build completion if autoscroll was active. !8391
+- Properly handle failed reCAPTCHA on user registration. !8403
+- Changed alerts to be responsive, centered text on smaller viewports. !8424 (Connor Smallman)
+- Pass Gitaly resource path to gitlab-workhorse if Gitaly is enabled. !8440
+- Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown. !8443
+- Don't instrument 405 Grape calls. !8445
+- Change CI template linter textarea with Ace Editor. !8452 (Didem Acet)
+- Removes unneeded `window` declaration in environments related code. !8456
+- API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`. !8457 (Panagiotis Atmatzidis, David Eisner)
+- Fix broken url on group avatar. !8464 (hogewest)
+- Fixes buttons not being accessible via the keyboard when creating new group. !8469
+- Restore backup correctly when "BACKUP" environment variable is passed. !8477
+- Add new endpoints for Time Tracking. !8483
+- Fix Compare page throws 500 error when any branch/reference is not selected. !8492 (Martin Cabrera)
+- Treat environments matching `production/*` as Production. !8500
+- Hide build artifacts keep button if operation is not allowed. !8501
+- Update the gitlab-markup gem to the version 1.5.1. !8509
+- Remove Lock Icon on Protected Tag. !8513 (Sergey Nikitin)
+- Use cached values to compute total issues count in milestone index pages. !8518
+- Speed up dashboard milestone index by scoping IssuesFinder to user authorized projects. !8524
+- Copy <some text> to clipboard. !8535
+- Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route. !8544
+- Fixes builds dropdown making request when clicked to be closed. !8545
+- Fixes pipeline status cell is too wide by adding missing classes in table head cells. !8549
+- Mutate the attribute instead of issuing a write operation to the DB in `ProjectFeaturesCompatibility` concern. !8552
+- Fix links to commits pages on pipelines list page. !8558
+- Ensure updating project settings shows a flash message on success. !8579 (Sandish Chen)
+- Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip. !8593
+- Autoresize markdown preview. !8607 (Didem Acet)
+- Link external build badge to its target URL. !8611
+- Adjust ProjectStatistic#repository_size with values saved as MB. !8616
+- Correct User-agent placement in robots.txt. !8623 (Eric Sabelhaus)
+- Record used SSH keys only once per day. !8655
+- Do not generate pipeline branch/tag path if not present. !8658
+- Fix Merge When Pipeline Succeeds immediate merge bug. !8685
+- Fix blame 500 error on invalid path. !25761 (Jeff Stubler)
+- Added animations to issue boards interactions.
+- Check if user can read project before being assigned to issue.
+- Show 'too many changes' message for created merge requests when they are too large.
+- Fix redirect after update file when user has forked project.
+- Parse JIRA issue references even if Issue Tracker is disabled.
+- Made download artifacts button accessible via keyboard by changing it from an anchor tag to an actual button. (Ryan Harris)
+- Make play button on Pipelines page accessible via keyboard. (Ryan Harris)
+- Decreases font-size on login page.
+- Fixed merge request tabs dont move when opening collapsed sidebar.
+- Display project avatars on Admin Area and Projects pages for mobile views. (Ryan Harris)
+- Fix participants margins to fit on one line.
+- 26352 Change Profile settings to User / Settings.
+- Fix Commits API to accept a Project path upon POST.
+- Expire related caches after changing HEAD. (Minqi Pan)
+- Add various hover animations throughout the application.
+- Re-order update steps in the 8.14 -> 8.15 upgrade guide.
+- Move award emoji's out of the discussion tab for merge requests.
+- Synchronize all project authorization refreshing work to prevent race conditions.
+- Remove the project_authorizations.id column.
+- Combined the settings options project members and groups into a single one called members.
+- Change earlier to task_status_short to avoid titlebar line wraps.
+- 25701 standardize text colors.
+- Handle HTTP errors in environment list.
+- Re-add Google Cloud Storage as a backup strategy.
+- Change status colors of runners to better defaults.
+- Added number_with_delimiter to counter on milestone panels. (Ryan Harris)
+- Query external CI statuses in the background.
+- Allow group and project paths when transferring projects via the API.
+- Don't validate environment urls on .gitlab-ci.yml.
+- Fix a Grape deprecation, use `#request_method` instead of `#route_method`.
+- Fill missing authorized projects rows.
+- Allow API query to find projects with dots in their name. (Bruno Melli)
+- Fix import/export wrong user mapping.
+- Removed bottom padding from merge manually from CLI because of repositioning award emoji's.
+- Fix project queued for deletion re-creation tooltip.
+- Fix search group/project filtering to show results.
+- Fix 500 error when POSTing to Users API with optional confirm param.
+- 26504 Fix styling of MR jump to discussion button.
+- Add margin to markdown math blocks.
+- Add hover state to MR comment reply button.
+
## 8.15.4 (2017-01-09)
- Make successful pipeline emails off for watchers. !8176
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 6085e946503..f0bb29e7638 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.2.1
+1.3.0
diff --git a/Gemfile b/Gemfile
index 9bf37891f74..4e9cf91c429 100644
--- a/Gemfile
+++ b/Gemfile
@@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
-gem 'omniauth', '~> 1.3.1'
+gem 'omniauth', '~> 1.3.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
@@ -219,7 +219,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
-gem 'sassc-rails', '~> 1.3.0'
+gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
@@ -252,7 +252,7 @@ end
group :development do
gem 'foreman', '~> 0.78.0'
- gem 'brakeman', '~> 3.3.0', require: false
+ gem 'brakeman', '~> 3.4.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'bullet', '~> 5.2.0', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 4d025683b20..c9115982838 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -88,7 +88,7 @@ GEM
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
- brakeman (3.3.2)
+ brakeman (3.4.1)
browser (2.2.0)
builder (3.2.2)
bullet (5.2.0)
@@ -449,7 +449,7 @@ GEM
octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4)
- omniauth (1.3.1)
+ omniauth (1.3.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
@@ -667,17 +667,12 @@ GEM
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.22)
- sassc (1.11.1)
- bundler
- ffi (~> 1.9.6)
- sass (>= 3.3.0)
- sassc-rails (1.3.0)
- railties (>= 4.0.0)
- sass
- sassc (~> 1.9)
- sprockets (> 2.11)
- sprockets-rails
- tilt
+ sass-rails (5.0.6)
+ railties (>= 4.0.0, < 6)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
@@ -849,7 +844,7 @@ DEPENDENCIES
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
- brakeman (~> 3.3.0)
+ brakeman (~> 3.4.0)
browser (~> 2.2)
bullet (~> 5.2.0)
bundler-audit (~> 0.5.0)
@@ -930,7 +925,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0)
octokit (~> 4.6.2)
oj (~> 2.17.4)
- omniauth (~> 1.3.1)
+ omniauth (~> 1.3.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6)
@@ -976,7 +971,7 @@ DEPENDENCIES
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
sanitize (~> 2.0)
- sassc-rails (~> 1.3.0)
+ sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
diff --git a/README.md b/README.md
index 4e28f3aacfd..4f85fac4a56 100644
--- a/README.md
+++ b/README.md
@@ -113,4 +113,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
-[These people](https://twitter.com/gitlab/favorites) seem to like it.
+[These people](https://twitter.com/gitlab/likes) seem to like it.
diff --git a/VERSION b/VERSION
index 8e9258150a9..5c99c061a47 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.16.0-pre
+8.17.0-pre
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 5a7d823e84c..993f427c9fb 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
/* global Turbolinks */
(function() {
@@ -61,7 +61,5 @@
}
return Admin;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index f60f27d1210..b4a8c827d7f 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */
(function() {
var Api = {
@@ -29,9 +29,9 @@
return $.ajax({
url: url,
data: $.extend({
- search: query,
- per_page: 20
- }, options),
+ search: query,
+ per_page: 20
+ }, options),
dataType: "json"
}).done(function(groups) {
return callback(groups);
@@ -73,7 +73,7 @@
return $.ajax({
url: url,
type: "POST",
- data: {'label': data},
+ data: { 'label': data },
dataType: "json"
}).done(function(label) {
return callback(label);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 9417afc2ea7..8438de6cdf1 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */
(function() {
this.Aside = (function() {
function Aside() {
@@ -21,7 +21,5 @@
}
return Aside;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index f45dbe4cbf2..b16a2c0f73a 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
(function() {
this.Autosave = (function() {
function Autosave(field, key) {
@@ -58,7 +58,5 @@
};
return Autosave;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 107a7978a87..629dc267337 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,9 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */
/* global Cookies */
(function() {
this.AwardsHandler = (function() {
- var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
+ var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
this.aliases = gl.emojiAliases();
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
@@ -134,7 +134,7 @@
return this.decrementCounter($emojiButton, emoji);
} else {
counter = $emojiButton.find('.js-counter');
- counter.text(parseInt(counter.text()) + 1);
+ counter.text(parseInt(counter.text(), 10) + 1);
$emojiButton.addClass('active');
this.addYouToUserList(votesBlock, emoji);
return this.animateEmoji($emojiButton);
@@ -211,10 +211,10 @@
};
AwardsHandler.prototype.toSentence = function(list) {
- if(list.length <= 2){
+ if (list.length <= 2) {
return list.join(' and ');
}
- else{
+ else {
return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
}
};
@@ -339,7 +339,7 @@
if (Cookies.get('frequently_used_emojis')) {
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
- for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) {
+ for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) {
emoji = frequentlyUsedEmojis[i];
$(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul);
}
@@ -374,7 +374,5 @@
};
return AwardsHandler;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index c62a4c5a456..a6bc262b657 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */
/* global autosize */
/*= require jquery.ba-resize */
@@ -26,5 +26,4 @@
autosize.update($fields);
return $fields.css('resize', 'vertical');
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 3998ee9a0a0..6af8f593872 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, max-len */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
@@ -23,5 +23,4 @@
return e.preventDefault();
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 586f941a6e3..d4895011be7 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, max-len */
// Quick Submit behavior
//
@@ -74,5 +74,4 @@
return $this.tooltip('hide');
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 72362988b2e..ccbd6b993cb 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, max-len */
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
@@ -59,5 +59,4 @@
return hideOrShowHelpBlock($form);
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index 57bd13eecf8..d3455fa3d8c 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,9 +1,8 @@
-/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign, comma-dangle */
/* global Api */
/*= require blob/template_selector */
((global) => {
-
class BlobCiYamlSelector extends gl.TemplateSelector {
requestFile(query) {
return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
@@ -39,5 +38,4 @@
}
global.BlobCiYamlSelectors = BlobCiYamlSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index eab686c45c3..04bfe363929 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */
/* global Dropzone */
(function() {
@@ -62,7 +62,5 @@
}
return BlobFileDropzone;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 15563e429a0..5fd0857db29 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */
/* global Api */
/*= require blob/template_selector */
@@ -19,7 +19,5 @@
};
return BlobGitignoreSelector;
-
})(gl.TemplateSelector);
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index d7f95093688..8236457f0f1 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */
/* global BlobGitignoreSelector */
(function() {
@@ -22,7 +22,5 @@
}
return BlobGitignoreSelectors;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index d9c6f65a083..7a14eb160d0 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */
/* global Api */
/*= require blob/template_selector */
@@ -24,7 +24,5 @@
};
return BlobLicenseSelector;
-
})(gl.TemplateSelector);
-
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
index 268640681d4..c5067b0feae 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */
+/* eslint-disable no-unused-vars, no-param-reassign */
/* global BlobLicenseSelector */
((global) => {
@@ -20,5 +20,4 @@
}
global.BlobLicenseSelectors = BlobLicenseSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 7a1ee9998c8..7e03ec3b391 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -1,101 +1,101 @@
-/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */
+/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */
((global) => {
- class TemplateSelector {
- constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
- this.onClick = this.onClick.bind(this);
- this.dropdown = dropdown;
- this.data = data;
- this.pattern = pattern;
- this.wrapper = wrapper;
- this.editor = editor;
- this.fileEndpoint = fileEndpoint;
- this.$input = $input || $('#file_name');
- this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
- this.buildDropdown();
- this.bindEvents();
- this.onFilenameUpdate();
+ class TemplateSelector {
+ constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
+ this.onClick = this.onClick.bind(this);
+ this.dropdown = dropdown;
+ this.data = data;
+ this.pattern = pattern;
+ this.wrapper = wrapper;
+ this.editor = editor;
+ this.fileEndpoint = fileEndpoint;
+ this.$input = $input || $('#file_name');
+ this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
+ this.buildDropdown();
+ this.bindEvents();
+ this.onFilenameUpdate();
- this.autosizeUpdateEvent = document.createEvent('Event');
- this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
- }
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+ }
- buildDropdown() {
- return this.dropdown.glDropdown({
- data: this.data,
- filterable: true,
- selectable: true,
- toggleLabel: this.toggleLabel,
- search: {
- fields: ['name']
- },
- clicked: this.onClick,
- text: function(item) {
- return item.name;
- }
- });
- }
+ buildDropdown() {
+ return this.dropdown.glDropdown({
+ data: this.data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: this.toggleLabel,
+ search: {
+ fields: ['name']
+ },
+ clicked: this.onClick,
+ text: function(item) {
+ return item.name;
+ }
+ });
+ }
- bindEvents() {
- return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
- }
+ bindEvents() {
+ return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
+ }
- toggleLabel(item) {
- return item.name;
- }
+ toggleLabel(item) {
+ return item.name;
+ }
- onFilenameUpdate() {
- var filenameMatches;
- if (!this.$input.length) {
- return;
- }
- filenameMatches = this.pattern.test(this.$input.val().trim());
- if (!filenameMatches) {
- this.wrapper.addClass('hidden');
- return;
- }
- return this.wrapper.removeClass('hidden');
+ onFilenameUpdate() {
+ var filenameMatches;
+ if (!this.$input.length) {
+ return;
}
-
- onClick(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
+ filenameMatches = this.pattern.test(this.$input.val().trim());
+ if (!filenameMatches) {
+ this.wrapper.addClass('hidden');
+ return;
}
+ return this.wrapper.removeClass('hidden');
+ }
- requestFile(item) {
- // This `requestFile` method is an abstract method that should
- // be added by all subclasses.
- }
+ onClick(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
- // To be implemented on the extending class
- // e.g.
- // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus } = {}) {
- if (!file) return;
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
- const oldValue = this.editor.getValue();
- let newValue = file.content;
+ // To be implemented on the extending class
+ // e.g.
+ // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+ requestFileSuccess(file, { skipFocus } = {}) {
+ if (!file) return;
- this.editor.setValue(newValue, 1);
- if (!skipFocus) this.editor.focus();
+ const oldValue = this.editor.getValue();
+ const newValue = file.content;
- if (this.editor instanceof jQuery) {
- this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
- }
- }
+ this.editor.setValue(newValue, 1);
+ if (!skipFocus) this.editor.focus();
- startLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
}
+ }
- stopLoadingSpinner() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
- }
+ startLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ stopLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
}
+ }
- global.TemplateSelector = TemplateSelector;
- })(window.gl || ( window.gl = {}));
+ global.TemplateSelector = TemplateSelector;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index 8c40e36a80a..dfad9b2122b 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
/* global NewCommitForm */
@@ -12,5 +12,4 @@
var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
new NewCommitForm($('.js-edit-blob-form'));
});
-
}).call(this);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index fa43ff611cc..079445e8278 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,9 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */
/* global ace */
/* global BlobGitignoreSelectors */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.EditBlob = (function() {
function EditBlob(assets_path, ace_mode) {
@@ -84,7 +84,5 @@
};
return EditBlob;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 2c9ab61c94d..f9766471780 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* global Vue */
/* global BoardService */
@@ -16,8 +16,8 @@
//= require ./vue_resource_interceptor
$(() => {
- const $boardApp = document.getElementById('board-app'),
- Store = gl.issueBoards.BoardsStore;
+ const $boardApp = document.getElementById('board-app');
+ const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 8d1a0f482f9..a32881116d5 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Vue */
/* global Sortable */
@@ -88,8 +88,8 @@
gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
- const order = this.sortable.toArray(),
- list = Store.findList('id', parseInt(e.item.dataset.id));
+ const order = this.sortable.toArray();
+ const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
this.$nextTick(() => {
Store.moveList(list, order);
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 0a47a22fad2..d76314c1892 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, comma-dangle, semi */
+/* eslint-disable space-before-function-paren, comma-dangle */
/* global Vue */
/* global ListLabel */
@@ -15,7 +15,7 @@
new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
new ListLabel({ title: 'Doing', color: '#5CB85C' })
]
- }
+ };
},
methods: {
addDefaultLists () {
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 6711930622b..630fe084175 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */
+/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Vue */
/* global Sortable */
@@ -43,7 +43,7 @@
issues () {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
- this.list.page++;
+ this.list.page += 1;
this.list.getIssues(false);
}
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 3f5cf8420a8..556826a9148 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */
+/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */
(() => {
window.gl = window.gl || {};
@@ -32,17 +32,17 @@
});
},
renderRow (label) {
- const active = Store.findList('title', label.title),
- $li = $('<li />'),
- $a = $('<a />', {
- class: (active ? `is-active js-board-list-${active.id}` : ''),
- text: label.title,
- href: '#'
- }),
- $labelColor = $('<span />', {
- class: 'dropdown-label-box',
- style: `background-color: ${label.color}`
- });
+ const active = Store.findList('title', label.title);
+ const $li = $('<li />');
+ const $a = $('<a />', {
+ class: (active ? `is-active js-board-list-${active.id}` : ''),
+ text: label.title,
+ href: '#'
+ });
+ const $labelColor = $('<span />', {
+ class: 'dropdown-label-box',
+ style: `background-color: ${label.color}`
+ });
return $li.append($a.prepend($labelColor));
},
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index d5859444a32..b6c6d17274f 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */
+/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
((w) => {
@@ -19,7 +19,7 @@
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
- let defaultSortOptions = {
+ const defaultSortOptions = {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
@@ -31,7 +31,7 @@
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
- }
+ };
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
return defaultSortOptions;
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index cd942c8332b..31531c3ee34 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */
+/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
/* global Vue */
/* global ListLabel */
/* global ListMilestone */
@@ -37,12 +37,12 @@ class ListIssue {
}
findLabel (findLabel) {
- return this.labels.filter( label => label.title === findLabel.title )[0];
+ return this.labels.filter(label => label.title === findLabel.title)[0];
}
removeLabel (removeLabel) {
if (removeLabel) {
- this.labels = this.labels.filter( label => removeLabel.title !== label.title );
+ this.labels = this.labels.filter(label => removeLabel.title !== label.title);
}
}
@@ -51,7 +51,7 @@ class ListIssue {
}
getLists () {
- return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
+ return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id));
}
update (url) {
@@ -60,7 +60,7 @@ class ListIssue {
milestone_id: this.milestone ? this.milestone.id : null,
due_date: this.dueDate,
assignee_id: this.assignee ? this.assignee.id : null,
- label_ids: this.labels.map( (label) => label.id )
+ label_ids: this.labels.map((label) => label.id)
}
};
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index fb13f529b11..3dd5f273057 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
/* global ListLabel */
@@ -58,7 +58,7 @@ class List {
nextPage () {
if (this.issuesSize > this.issues.length) {
- this.page++;
+ this.page += 1;
return this.getIssues(false);
}
@@ -66,12 +66,12 @@ class List {
getIssues (emptyIssues = true) {
const filters = this.filters;
- let data = { page: this.page };
+ const data = { page: this.page };
Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
if (this.label) {
- data.label_name = data.label_name.filter( label => label !== this.label.title );
+ data.label_name = data.label_name.filter(label => label !== this.label.title);
}
if (emptyIssues) {
@@ -94,7 +94,7 @@ class List {
newIssue (issue) {
this.addIssue(issue);
- this.issuesSize++;
+ this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
.then((resp) => {
@@ -122,7 +122,7 @@ class List {
}
if (listFrom) {
- this.issuesSize++;
+ this.issuesSize += 1;
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
@@ -132,7 +132,7 @@ class List {
}
findIssue (id) {
- return this.issues.filter( issue => issue.id === id )[0];
+ return this.issues.filter(issue => issue.id === id)[0];
}
removeIssue (removeIssue) {
@@ -140,7 +140,7 @@ class List {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
- this.issuesSize--;
+ this.issuesSize -= 1;
issue.removeLabel(this.label);
}
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index f18633f0913..ea55158306b 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */
+/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
/* global Vue */
class BoardService {
@@ -47,7 +47,7 @@ class BoardService {
}
getIssuesForList (id, filter = {}) {
- let data = { id };
+ const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
return this.issues.get(data);
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index e7a14ea5bca..cdf1b09c0a4 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
/* global Cookies */
/* global List */
@@ -33,8 +33,8 @@
return list;
},
new (listObj) {
- const list = this.addList(listObj),
- backlogList = this.findList('type', 'backlog', 'backlog');
+ const list = this.addList(listObj);
+ const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
@@ -52,7 +52,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
+ return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -82,20 +82,20 @@
if (!list) return;
- this.state.lists = this.state.lists.filter( list => list.id !== id );
+ this.state.lists = this.state.lists.filter(list => list.id !== id);
},
moveList (listFrom, orderLists) {
orderLists.forEach((id, i) => {
- const list = this.findList('id', parseInt(id));
+ const list = this.findList('id', parseInt(id, 10));
list.position = i;
});
listFrom.update();
},
moveIssueToList (listFrom, listTo, issue, newIndex) {
- const issueTo = listTo.findIssue(issue.id),
- issueLists = issue.getLists(),
- listLabels = issueLists.map( listIssue => listIssue.label );
+ const issueTo = listTo.findIssue(issue.id);
+ const issueLists = issue.getLists();
+ const listLabels = issueLists.map(listIssue => listIssue.label);
// Add to new lists issues if it doesn't already exist
if (!issueTo) {
@@ -105,7 +105,7 @@
if (listTo.type === 'done' && listFrom.type !== 'backlog') {
issueLists.forEach((list) => {
list.removeIssue(issue);
- })
+ });
issue.removeLabels(listLabels);
} else {
listFrom.removeIssue(issue);
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 01e09ec482e..f05780167bf 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -1,120 +1,119 @@
-/* eslint-disable wrap-iife, func-names, strict, indent, no-tabs, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, object-curly-spacing, no-unused-expressions, prefer-arrow-callback, max-len */
+/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */
(function () {
- 'use strict';
-
- function simulateEvent(el, type, options) {
- var event;
- if (!el) return;
- var ownerDocument = el.ownerDocument;
-
- options = options || {};
-
- if (/^mouse/.test(type)) {
- event = ownerDocument.createEvent('MouseEvents');
- event.initMouseEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
- } else {
- event = ownerDocument.createEvent('CustomEvent');
-
- event.initCustomEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
-
- event.dataTransfer = {
- data: {},
-
- setData: function (type, val) {
- this.data[type] = val;
- },
-
- getData: function (type) {
- return this.data[type];
- }
- };
- }
-
- if (el.dispatchEvent) {
- el.dispatchEvent(event);
- } else if (el.fireEvent) {
- el.fireEvent('on' + type, event);
- }
-
- return event;
- }
-
- function getTraget(target) {
- var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- var children = el.children;
-
- return (
- children[target.index] ||
- children[target.index === 'first' ? 0 : -1] ||
- children[target.index === 'last' ? children.length - 1 : -1]
- );
- }
-
- function getRect(el) {
- var rect = el.getBoundingClientRect();
- var width = rect.right - rect.left;
- var height = rect.bottom - rect.top;
-
- return {
- x: rect.left,
- y: rect.top,
- cx: rect.left + width / 2,
- cy: rect.top + height / 2,
- w: width,
- h: height,
- hw: width / 2,
- wh: height / 2
- };
- }
-
- function simulateDrag(options, callback) {
- options.to.el = options.to.el || options.from.el;
-
- var fromEl = getTraget(options.from);
- var toEl = getTraget(options.to);
+ 'use strict';
+
+ function simulateEvent(el, type, options) {
+ var event;
+ if (!el) return;
+ var ownerDocument = el.ownerDocument;
+
+ options = options || {};
+
+ if (/^mouse/.test(type)) {
+ event = ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData: function (type, val) {
+ this.data[type] = val;
+ },
+
+ getData: function (type) {
+ return this.data[type];
+ }
+ };
+ }
+
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent('on' + type, event);
+ }
+
+ return event;
+ }
+
+ function getTraget(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1]
+ );
+ }
+
+ function getRect(el) {
+ var rect = el.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ var height = rect.bottom - rect.top;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + width / 2,
+ cy: rect.top + height / 2,
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2
+ };
+ }
+
+ function simulateDrag(options, callback) {
+ options.to.el = options.to.el || options.from.el;
+
+ var fromEl = getTraget(options.from);
+ var toEl = getTraget(options.to);
var scrollable = options.scrollable;
- var fromRect = getRect(fromEl);
- var toRect = getRect(toEl);
-
- var startTime = new Date().getTime();
- var duration = options.duration || 1000;
- simulateEvent(fromEl, 'mousedown', {button: 0});
- options.ontap && options.ontap();
- window.SIMULATE_DRAG_ACTIVE = 1;
-
- var dragInterval = setInterval(function loop() {
- var progress = (new Date().getTime() - startTime) / duration;
- var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
- var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
- var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
-
- simulateEvent(overEl, 'mousemove', {
- clientX: x,
- clientY: y
- });
-
- if (progress >= 1) {
- options.ondragend && options.ondragend();
- simulateEvent(toEl, 'mouseup');
- clearInterval(dragInterval);
- window.SIMULATE_DRAG_ACTIVE = 0;
- }
- }, 100);
-
- return {
- target: fromEl,
- fromList: fromEl.parentNode,
- toList: toEl.parentNode
- };
- }
-
-
- // Export
- window.simulateEvent = simulateEvent;
- window.simulateDrag = simulateDrag;
+ var fromRect = getRect(fromEl);
+ var toRect = getRect(toEl);
+
+ var startTime = new Date().getTime();
+ var duration = options.duration || 1000;
+ simulateEvent(fromEl, 'mousedown', { button: 0 });
+ options.ontap && options.ontap();
+ window.SIMULATE_DRAG_ACTIVE = 1;
+
+ var dragInterval = setInterval(function loop() {
+ var progress = (new Date().getTime() - startTime) / duration;
+ var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
+ var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+ var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y
+ });
+
+ if (progress >= 1) {
+ options.ondragend && options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode
+ };
+ }
+
+ // Export
+ window.simulateEvent = simulateEvent;
+ window.simulateDrag = simulateDrag;
})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index 3723a2039f9..54c2b4ad369 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,10 +1,10 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */
/* global Vue */
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next(function (response) {
- Vue.activeResources--;
+ Vue.activeResources -= 1;
});
});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index a7e72430141..eae062a3aa3 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */
(function() {
var Breakpoints = (function() {
@@ -52,7 +52,6 @@
};
return BreakpointInstance;
-
})();
Breakpoints.get = function() {
@@ -60,7 +59,6 @@
};
return Breakpoints;
-
})();
$((function(_this) {
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index 30432dae278..dbdadc73c3f 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
(function() {
$(function() {
var previewPath;
@@ -31,5 +31,4 @@
}
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index fca47002870..0df84234520 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,9 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* global Breakpoints */
/* global Turbolinks */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
var AUTO_SCROLL_OFFSET = 75;
var DOWN_BUILD_TRACE = '#down-build-trace';
@@ -70,7 +70,7 @@
this.$sidebar = $('.js-build-sidebar');
this.sidebarTranslationLimits = {
min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
- }
+ };
this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
this.$sidebar.css({
top: this.sidebarTranslationLimits.max
@@ -85,7 +85,7 @@
};
Build.prototype.getInitialBuildTrace = function() {
- var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
return $.ajax({
url: this.buildUrl,
@@ -153,7 +153,7 @@
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
this.$autoScrollContainer.hide();
- }
+ };
// Page scroll listener to detect if user has scrolling page
// and handle following cases
@@ -291,7 +291,5 @@
};
return Build;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index c423a548a30..083448552b6 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */
(function() {
this.BuildArtifacts = (function() {
function BuildArtifacts() {
@@ -22,7 +22,5 @@
};
return BuildArtifacts;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
index 993424d422f..99082b412e2 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */
-$(function(){
- $('.reveal-variables').off('click').on('click',function(){
+$(function() {
+ $('.reveal-variables').off('click').on('click', function() {
$('.js-build').toggle().niceScroll();
$(this).hide();
});
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 67b33a4d7ee..c656ae4e241 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
(function() {
@@ -10,7 +10,5 @@
}
return Commit;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 27512312c7c..184b4561d2e 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
/* global ImageFile */
(function() {
@@ -10,7 +10,5 @@
}
return CommitFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index fd8910e916f..f09a6b1e676 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
(function() {
gl.ImageFile = (function() {
var prepareFrames;
@@ -172,7 +172,5 @@
};
return ImageFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 24a6e4ff0e9..cabeae74ae3 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */
/* global Pager */
(function() {
@@ -58,7 +58,5 @@
};
return CommitsList;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index d4243baadb5..9591df70e9c 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,4 +1,4 @@
-/* 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, padded-blocks, max-len */
+/* 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 */
(function() {
this.Compare = (function() {
function Compare(opts) {
@@ -87,7 +87,5 @@
};
return Compare;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index 251f2ded347..3587431ab69 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
(function() {
this.CompareAutocomplete = (function() {
@@ -28,7 +28,6 @@
selectable: true,
filterable: true,
filterByText: true,
- toggleLabel: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
@@ -66,7 +65,5 @@
};
return CompareAutocomplete;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 686a48486f3..35d98492012 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
(function() {
this.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
@@ -27,7 +27,5 @@
}
return ConfirmDangerModal;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 6a13f38588d..3485f8f91ed 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
/* global Clipboard */
/*= require clipboard */
@@ -46,5 +46,4 @@
clipboard.on('success', genericSuccess);
return clipboard.on('error', genericError);
});
-
}).call(this);
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index c59d3996fab..2514459e65e 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
/* global Vue */
/* global CommentsStore */
@@ -10,7 +10,7 @@
data() {
return {
textareaIsEmpty: true
- }
+ };
},
computed: {
discussion: function () {
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index f47867fc3b0..c3898873eaa 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global Vue */
/* global DiscussionMixins */
/* global CommentsStore */
@@ -46,13 +46,13 @@
},
methods: {
jumpToNextUnresolvedDiscussion: function () {
- let discussionsSelector,
- discussionIdsInScope,
- firstUnresolvedDiscussionId,
- nextUnresolvedDiscussionId,
- activeTab = window.mrTabs.currentAction,
- hasDiscussionsToJumpTo = true,
- jumpToFirstDiscussion = !this.discussionId;
+ let discussionsSelector;
+ let discussionIdsInScope;
+ let firstUnresolvedDiscussionId;
+ let nextUnresolvedDiscussionId;
+ let activeTab = window.mrTabs.currentAction;
+ let hasDiscussionsToJumpTo = true;
+ let jumpToFirstDiscussion = !this.discussionId;
const discussionIdsForElements = function(elements) {
return elements.map(function() {
@@ -68,11 +68,11 @@
let unresolvedDiscussionCount = 0;
- for (let i = 0; i < discussionIdsInScope.length; i++) {
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
if (discussion && !discussion.isResolved()) {
- unresolvedDiscussionCount++;
+ unresolvedDiscussionCount += 1;
}
}
@@ -109,7 +109,7 @@
}
let currentDiscussionFound = false;
- for (let i = 0; i < discussionIdsInScope.length; i++) {
+ for (let i = 0; i < discussionIdsInScope.length; i += 1) {
const discussionId = discussionIdsInScope[i];
const discussion = discussions[discussionId];
@@ -156,7 +156,7 @@
// If the next discussion is closed, toggle it open.
if ($target.find('.js-toggle-content').is(':hidden')) {
- $target.find('.js-toggle-button i').trigger('click')
+ $target.find('.js-toggle-button i').trigger('click');
}
} else if (activeTab === 'diffs') {
// Resolved discussions are hidden in the diffs tab by default.
@@ -170,7 +170,7 @@
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
// 4 diff lines above it: the line the discussion was in response to + 3 context
let prevEl;
- for (let i = 0; i < 4; i++) {
+ for (let i = 0; i < 4; i += 1) {
prevEl = $target.prev();
// If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index 840b5aa922e..1b3a57d0962 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -2,8 +2,6 @@
/* global Vue */
/* global ResolveCount */
-//= require vue
-//= require vue-resource
//= require_directory ./models
//= require_directory ./stores
//= require_directory ./services
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
index a9ea18bf82b..3c08c222f46 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
((w) => {
w.DiscussionMixins = {
@@ -13,7 +13,7 @@
const discussion = this.discussions[discussionId];
if (discussion.isResolved()) {
- resolvedCount++;
+ resolvedCount += 1;
}
}
@@ -26,7 +26,7 @@
const discussion = this.discussions[discussionId];
if (!discussion.isResolved()) {
- unresolvedCount++;
+ unresolvedCount += 1;
}
}
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index 78c74985f78..a52c476352d 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */
+/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
/* global Vue */
/* global Flash */
/* global CommentsStore */
@@ -32,8 +32,8 @@
}
toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
- const discussion = CommentsStore.state[discussionId],
- isResolved = discussion.isResolved();
+ const discussion = CommentsStore.state[discussionId];
+ const isResolved = discussion.isResolved();
let promise;
if (isResolved) {
@@ -59,7 +59,7 @@
} else {
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
}
- })
+ });
}
resolveAll(projectPath, mergeRequestId, discussionId) {
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
index 1a7abbc6f75..c80d979b977 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
/* global Vue */
/* global DiscussionModel */
@@ -41,7 +41,7 @@
}
},
unresolvedDiscussionIds: function () {
- let ids = [];
+ const ids = [];
for (const discussionId in this.state) {
const discussion = this.state[discussionId];
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 99a34651639..529d476ca4e 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
@@ -261,6 +261,9 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
+ case 'help:index':
+ gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
+ break;
case 'search:show':
new Search();
break;
@@ -372,7 +375,5 @@
};
return Dispatcher;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js
index ed545ec8748..8b14191395b 100644
--- a/app/assets/javascripts/droplab/droplab.js
+++ b/app/assets/javascripts/droplab/droplab.js
@@ -58,10 +58,12 @@ var CustomEvent = require('./custom_event_polyfill');
var utils = require('./utils');
var DropDown = function(list) {
+ this.currentIndex = 0;
this.hidden = true;
this.list = list;
this.items = [];
this.getItems();
+ this.initTemplateString();
this.addEvents();
this.initialState = list.innerHTML;
};
@@ -72,6 +74,17 @@ Object.assign(DropDown.prototype, {
return this.items;
},
+ initTemplateString: function() {
+ var items = this.items || this.getItems();
+
+ var templateString = '';
+ if(items.length > 0) {
+ templateString = items[items.length - 1].outerHTML;
+ }
+ this.templateString = templateString;
+ return this.templateString;
+ },
+
clickEvent: function(e) {
// climb up the tree to find the LI
var selected = utils.closest(e.target, 'LI');
@@ -111,30 +124,21 @@ Object.assign(DropDown.prototype, {
addData: function(data) {
this.data = (this.data || []).concat(data);
- this.render(data);
+ this.render(this.data);
},
// call render manually on data;
render: function(data){
// debugger
// empty the list first
- var sampleItem;
+ var templateString = this.templateString;
var newChildren = [];
var toAppend;
- for(var i = 0; i < this.items.length; i++) {
- var item = this.items[i];
- sampleItem = item;
- if(item.parentNode && item.parentNode.dataset.hasOwnProperty('dynamic')) {
- item.parentNode.removeChild(item);
- }
- }
-
- newChildren = this.data.map(function(dat){
- var html = utils.t(sampleItem.outerHTML, dat);
+ newChildren = (data ||[]).map(function(dat){
+ var html = utils.t(templateString, dat);
var template = document.createElement('div');
template.innerHTML = html;
- // console.log(template.content)
// Help set the image src template
var imageTags = template.querySelectorAll('img[data-src]');
@@ -156,27 +160,30 @@ Object.assign(DropDown.prototype, {
if(toAppend) {
toAppend.innerHTML = newChildren.join('');
} else {
- this.list.innerHTML = newChildren.join('');
+ this.list.innerHTML = newChildren.join('');
}
},
show: function() {
- // debugger
- this.list.style.display = 'block';
- this.hidden = false;
+ if (this.hidden) {
+ // debugger
+ this.list.style.display = 'block';
+ this.currentIndex = 0;
+ this.hidden = false;
+ }
},
hide: function() {
- // debugger
- this.list.style.display = 'none';
- this.hidden = true;
- },
-
- destroy: function() {
if (!this.hidden) {
- this.hide();
+ // debugger
+ this.list.style.display = 'none';
+ this.currentIndex = 0;
+ this.hidden = true;
}
+ },
+ destroy: function() {
+ this.hide();
this.list.removeEventListener('click', this.clickWrapper);
}
});
@@ -278,7 +285,7 @@ require('./window')(function(w){
self.hooks[i].list.hide();
}
}.bind(this);
- w.addEventListener('click', this.windowClickedWrapper);
+ document.addEventListener('click', this.windowClickedWrapper);
},
removeEvents: function(){
@@ -307,7 +314,7 @@ require('./window')(function(w){
if(!list){
list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]);
}
-
+
if(hook) {
if(hook.tagName === 'A' || hook.tagName === 'BUTTON') {
this.hooks.push(new HookButton(hook, list, plugins, config));
@@ -462,6 +469,8 @@ Object.assign(HookInput.prototype, {
var self = this;
this.mousedown = function mousedown(e) {
+ if(self.hasRemovedEvents) return;
+
var mouseEvent = new CustomEvent('mousedown.dl', {
detail: {
hook: self,
@@ -474,6 +483,10 @@ Object.assign(HookInput.prototype, {
}
this.input = function input(e) {
+ if(self.hasRemovedEvents) return;
+
+ self.list.show();
+
var inputEvent = new CustomEvent('input.dl', {
detail: {
hook: self,
@@ -483,18 +496,23 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(inputEvent);
- self.list.show();
}
this.keyup = function keyup(e) {
+ if(self.hasRemovedEvents) return;
+
keyEvent(e, 'keyup.dl');
}
this.keydown = function keydown(e) {
+ if(self.hasRemovedEvents) return;
+
keyEvent(e, 'keydown.dl');
}
function keyEvent(e, keyEventName){
+ self.list.show();
+
var keyEvent = new CustomEvent(keyEventName, {
detail: {
hook: self,
@@ -506,7 +524,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(keyEvent);
- self.list.show();
}
this.events = this.events || {};
@@ -520,7 +537,8 @@ Object.assign(HookInput.prototype, {
this.trigger.addEventListener('keydown', this.keydown);
},
- removeEvents: function(){
+ removeEvents: function() {
+ this.hasRemovedEvents = true;
this.trigger.removeEventListener('mousedown', this.mousedown);
this.trigger.removeEventListener('input', this.input);
this.trigger.removeEventListener('keyup', this.keyup);
@@ -563,24 +581,43 @@ require('./window')(function(w){
module.exports = function(){
var currentKey;
var currentFocus;
- var currentIndex = 0;
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
- var listItems = list.list.querySelectorAll('li');
+ var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
+ var listItemsTmp = [];
for(var i = 0; i < listItems.length; i++) {
- listItems[i].classList.remove('dropdown-active');
+ var listItem = listItems[i];
+ listItem.classList.remove('dropdown-active');
+
+ if (listItem.style.display !== 'none') {
+ listItemsTmp.push(listItem);
+ }
}
- return listItems;
+ return listItemsTmp;
};
var setMenuForArrows = function setMenuForArrows(list) {
var listItems = removeHighlight(list);
- if(currentIndex>0){
- if(!listItems[currentIndex-1]){
- currentIndex = currentIndex-1;
+ if(list.currentIndex>0){
+ if(!listItems[list.currentIndex-1]){
+ list.currentIndex = list.currentIndex-1;
+ }
+
+ if (listItems[list.currentIndex-1]) {
+ var el = listItems[list.currentIndex-1];
+ var filterDropdownEl = el.closest('.filter-dropdown');
+ el.classList.add('dropdown-active');
+
+ if (filterDropdownEl) {
+ var filterDropdownBottom = filterDropdownEl.offsetHeight;
+ var elOffsetTop = el.offsetTop - 30;
+
+ if (elOffsetTop > filterDropdownBottom) {
+ filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
+ }
+ }
}
- listItems[currentIndex-1].classList.add('dropdown-active');
}
};
@@ -588,13 +625,13 @@ require('./window')(function(w){
var list = e.detail.hook.list;
removeHighlight(list);
list.show();
- currentIndex = 0;
+ list.currentIndex = 0;
isUpArrow = false;
isDownArrow = false;
};
var selectItem = function selectItem(list) {
var listItems = removeHighlight(list);
- var currentItem = listItems[currentIndex-1];
+ var currentItem = listItems[list.currentIndex-1];
var listEvent = new CustomEvent('click.dl', {
detail: {
list: list,
@@ -608,6 +645,8 @@ require('./window')(function(w){
var keydown = function keydown(e){
var typedOn = e.target;
+ var list = e.detail.hook.list;
+ var currentIndex = list.currentIndex;
isUpArrow = false;
isDownArrow = false;
@@ -630,7 +669,7 @@ require('./window')(function(w){
return;
}
if(currentKey === 'ArrowUp') {
- isUpArrow = true;
+ isUpArrow = true;
}
if(currentKey === 'ArrowDown') {
isDownArrow = true;
@@ -639,6 +678,7 @@ require('./window')(function(w){
if(isUpArrow){ currentIndex--; }
if(isDownArrow){ currentIndex++; }
if(currentIndex < 0){ currentIndex = 0; }
+ list.currentIndex = currentIndex;
setMenuForArrows(e.detail.hook.list);
};
@@ -668,16 +708,16 @@ var camelize = function(str) {
};
var closest = function(thisTag, stopTag) {
- while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){
+ while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){
thisTag = thisTag.parentNode;
}
return thisTag;
};
var isDropDownParts = function(target) {
- if(target.tagName === 'HTML') { return false; }
+ if(!target || target.tagName === 'HTML') { return false; }
return (
- target.hasAttribute(DATA_TRIGGER) ||
+ target.hasAttribute(DATA_TRIGGER) ||
target.hasAttribute(DATA_DROPDOWN)
);
};
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index f20610b3811..f7fed0987a2 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -29,6 +29,7 @@ require('../window')(function(w){
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
+ this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
@@ -52,19 +53,26 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
- var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
+ var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
- hook.list[config.method].call(hook.list, d);
+
+ if (!self.hook.list.hidden) {
+ self.hook.list[config.method].call(self.hook.list, d);
+ }
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
+ if (this.listTemplate) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ dynamicList.outerHTML = this.listTemplate;
+ }
}
};
});
@@ -76,4 +84,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
index af163f76851..86a08d0d01d 100644
--- a/app/assets/javascripts/droplab/droplab_ajax_filter.js
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -93,6 +93,7 @@ require('../window')(function(w){
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
+ self.hook.list.currentIndex = 0;
});
},
@@ -142,4 +143,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
index 41a220831f9..9b40a3f20a4 100644
--- a/app/assets/javascripts/droplab/droplab_filter.js
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -6,6 +6,8 @@ require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
+ var hiddenCount = 0;
+ var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
@@ -27,10 +29,22 @@ require('../window')(function(w){
};
}
+ dataHiddenCount = data.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
matches = data.map(function(o) {
return filterFunction(o, value);
});
- list.render(matches);
+
+ hiddenCount = matches.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ if (dataHiddenCount !== hiddenCount) {
+ list.render(matches);
+ list.currentIndex = 0;
+ }
},
init: function init(hookInput) {
@@ -57,4 +71,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 56cb39be642..3d183f4ecb4 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */
/* global Dropzone */
/*= require preview_markdown */
@@ -120,7 +120,7 @@
if (item.type.indexOf("image") !== -1) {
return item;
}
- i++;
+ i += 1;
}
return false;
};
@@ -215,7 +215,5 @@
}
return DropzoneInput;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index 201f9fdc3fe..d81d4cf8425 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
(function(global) {
class DueDateSelect {
@@ -16,7 +16,7 @@
this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
this.fieldName = $dropdown.data('field-name'),
this.abilityName = $dropdown.data('ability-name'),
- this.issueUpdateURL = $dropdown.data('issue-update')
+ this.issueUpdateURL = $dropdown.data('issue-update');
this.rawSelectedDate = null;
this.displayedDate = null;
@@ -135,7 +135,6 @@
return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden');
-
}
}).done((data) => {
if (isDropdown) {
@@ -179,5 +178,4 @@
}
global.DueDateSelectors = DueDateSelectors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 8b6fafb6104..fea642467fa 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable no-param-reassign */
+/* eslint-disable no-param-reassign, no-new */
/* global Vue */
/* global EnvironmentsService */
+/* global Flash */
//= require vue
//= require vue-resource
@@ -10,41 +11,6 @@
(() => {
window.gl = window.gl || {};
- /**
- * Given the visibility prop provided by the url query parameter and which
- * changes according to the active tab we need to filter which environments
- * should be visible.
- *
- * The environments array is a recursive tree structure and we need to filter
- * both root level environments and children environments.
- *
- * In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
- * functions work together.
- * The first one works as the filter that verifies if the given environment matches
- * the given state.
- * The second guarantees both root level and children elements are filtered as well.
- */
-
- const filterState = state => environment => environment.state === state && environment;
- /**
- * Given the filter function and the array of environments will return only
- * the environments that match the state provided to the filter function.
- *
- * @param {Function} fn
- * @param {Array} array
- * @return {Array}
- */
- const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
- if (item.children) {
- const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
- if (filteredChildren.length) {
- item.children = filteredChildren;
- return item;
- }
- }
- return fn(item);
- }).filter(Boolean);
-
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: {
store: {
@@ -81,10 +47,6 @@
},
computed: {
- filteredEnvironments() {
- return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
- },
-
scope() {
return this.$options.getQueryParameter('scope');
},
@@ -111,7 +73,7 @@
const scope = this.$options.getQueryParameter('scope');
if (scope) {
- this.visibility = scope;
+ this.store.storeVisibility(scope);
}
this.isLoading = true;
@@ -121,6 +83,10 @@
.then((json) => {
this.store.storeEnvironments(json);
this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the environments.', 'alert');
});
},
@@ -188,7 +154,7 @@
<div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title">
+ <h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
@@ -202,13 +168,13 @@
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
- class="btn btn-create">
+ class="btn btn-create js-new-environment-button">
New Environment
</a>
</div>
<div class="table-holder"
- v-if="!isLoading && state.environments.length > 0">
+ v-if="!isLoading && state.filteredEnvironments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
@@ -221,7 +187,7 @@
</tr>
</thead>
<tbody>
- <template v-for="model in filteredEnvironments"
+ <template v-for="model in state.filteredEnvironments"
v-bind:model="model">
<tr
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 9f24a6a4f88..3b003f6f661 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -3,7 +3,6 @@
//= require ./components/environment
//= require ./vue_resource_interceptor
-
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
index 0204a903ab5..9b4090100da 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js.es6
+++ b/app/assets/javascripts/environments/stores/environments_store.js.es6
@@ -10,6 +10,8 @@
this.state.environments = [];
this.state.stoppedCounter = 0;
this.state.availableCounter = 0;
+ this.state.visibility = 'available';
+ this.state.filteredEnvironments = [];
return this;
},
@@ -59,7 +61,7 @@
if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment);
- acc[acc.indexOf(occurs[0])].children.sort(this.sortByName);
+ acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else {
acc.push({
name: environment.environment_type,
@@ -73,13 +75,70 @@
}
return acc;
- }, []).sort(this.sortByName);
+ }, []).slice().sort(this.sortByName);
this.state.environments = environmentsTree;
+ this.filterEnvironmentsByVisibility(this.state.environments);
+
return environmentsTree;
},
+ storeVisibility(visibility) {
+ this.state.visibility = visibility;
+ },
+ /**
+ * Given the visibility prop provided by the url query parameter and which
+ * changes according to the active tab we need to filter which environments
+ * should be visible.
+ *
+ * The environments array is a recursive tree structure and we need to filter
+ * both root level environments and children environments.
+ *
+ * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
+ * functions work together.
+ * The first one works as the filter that verifies if the given environment matches
+ * the given state.
+ * The second guarantees both root level and children elements are filtered as well.
+ *
+ * Given array of environments will return only
+ * the environments that match the state stored.
+ *
+ * @param {Array} array
+ * @return {Array}
+ */
+ filterEnvironmentsByVisibility(arr) {
+ const filteredEnvironments = arr.map((item) => {
+ if (item.children) {
+ const filteredChildren = this.filterEnvironmentsByVisibility(
+ item.children,
+ ).filter(Boolean);
+
+ if (filteredChildren.length) {
+ item.children = filteredChildren;
+ return item;
+ }
+ }
+
+ return this.filterState(this.state.visibility, item);
+ }).filter(Boolean);
+
+ this.state.filteredEnvironments = filteredEnvironments;
+ return filteredEnvironments;
+ },
+
+ /**
+ * Given the state and the environment,
+ * returns only if the environment state matches the one provided.
+ *
+ * @param {String} state
+ * @param {Object} environment
+ * @return {Object}
+ */
+ filterState(state, environment) {
+ return environment.state === state && environment;
+ },
+
/**
* Toggles folder open property given the environment type.
*
diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6
index 717566a4715..cd401277689 100644
--- a/app/assets/javascripts/extensions/array.js.es6
+++ b/app/assets/javascripts/extensions/array.js.es6
@@ -1,11 +1,11 @@
-/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */
+/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, max-len */
Array.prototype.first = function() {
return this[0];
-}
+};
Array.prototype.last = function() {
return this[this.length-1];
-}
+};
Array.prototype.find = Array.prototype.find || function(predicate, ...args) {
if (!this) throw new TypeError('Array.prototype.find called on null or undefined');
diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6
new file mode 100644
index 00000000000..abedae4c1c7
--- /dev/null
+++ b/app/assets/javascripts/extensions/custom_event.js.es6
@@ -0,0 +1,12 @@
+/* global CustomEvent */
+/* eslint-disable no-global-assign */
+
+// Custom event support for IE
+CustomEvent = function CustomEvent(event, parameters) {
+ const params = parameters || { bubbles: false, cancelable: false, detail: undefined };
+ const evt = document.createEvent('CustomEvent');
+ evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
+ return evt;
+};
+
+CustomEvent.prototype = window.Event.prototype;
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
index 3f12ad9ff9f..90ab79305a7 100644
--- a/app/assets/javascripts/extensions/element.js.es6
+++ b/app/assets/javascripts/extensions/element.js.es6
@@ -1,5 +1,5 @@
/* global Element */
-/* eslint-disable consistent-return, max-len, no-empty, no-plusplus, func-names */
+/* eslint-disable consistent-return, max-len, no-empty, func-names */
Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
if (!selectedElement) return;
@@ -14,7 +14,7 @@ Element.prototype.matches = Element.prototype.matches ||
Element.prototype.webkitMatchesSelector ||
function (s) {
const matches = (this.document || this.ownerDocument).querySelectorAll(s);
- let i = matches.length;
- while (--i >= 0 && matches.item(i) !== this) {}
+ let i = matches.length - 1;
+ while (i >= 0 && matches.item(i) !== this) { i -= 1; }
return i > -1;
};
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index cdedc865d1b..d3b58b2707a 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
@@ -13,5 +13,4 @@
return $(this).removeAttr('disabled').removeClass('disabled');
}
});
-
}).call(this);
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 785f2869970..895a872568d 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
@@ -132,7 +132,6 @@
};
return FilesCommentButton;
-
})();
$.fn.filesCommentButton = function() {
@@ -145,5 +144,4 @@
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
index 63c20f57520..7d297b8eee8 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
@@ -9,7 +9,7 @@
this.config = {
droplabFilter: {
template: 'hint',
- filterFunction: gl.DropdownUtils.filterHint,
+ filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
},
};
}
@@ -20,6 +20,9 @@
if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) {
this.dismissDropdown();
+ } else if (selected.getAttribute('data-action') === 'submit') {
+ this.dismissDropdown();
+ this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
index f06c3fc9c6f..13cbec1be4a 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
@@ -15,7 +15,7 @@
loadingTemplate: this.loadingTemplate,
},
droplabFilter: {
- filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol),
+ filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
},
};
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
index e80d266ae89..7bf199d9274 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -37,7 +37,7 @@
}
getSearchInput() {
- const query = this.input.value.trim();
+ const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
return lastToken.value || '';
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
index c27ef3042d1..eeab10fba17 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -20,17 +20,15 @@
return escapedText;
}
- static filterWithSymbol(filterSymbol, item, query) {
+ static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
+ const query = gl.DropdownUtils.getSearchInput(input);
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase();
-
- if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
- value = value.slice(1);
- }
+ value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -1));
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
@@ -44,8 +42,9 @@
return updatedItem;
}
- static filterHint(item, query) {
+ static filterHint(input, item) {
const updatedItem = item;
+ const query = gl.DropdownUtils.getSearchInput(input);
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
lastToken = lastToken.key || lastToken || '';
@@ -72,6 +71,48 @@
// Return boolean based on whether it was set
return dataValue !== null;
}
+
+ static getSearchInput(filteredSearchInput) {
+ const inputValue = filteredSearchInput.value;
+ const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
+
+ return inputValue.slice(0, right);
+ }
+
+ static getInputSelectionPosition(input) {
+ const selectionStart = input.selectionStart;
+ let inputValue = input.value;
+ // Replace all spaces inside quote marks with underscores
+ // This helps with matching the beginning & end of a token:key
+ inputValue = inputValue.replace(/("(.*?)"|:\s+)/g, str => str.replace(/\s/g, '_'));
+
+ // Get the right position for the word selected
+ // Regex matches first space
+ let right = inputValue.slice(selectionStart).search(/\s/);
+
+ if (right >= 0) {
+ right += selectionStart;
+ } else if (right < 0) {
+ right = inputValue.length;
+ }
+
+ // Get the left position for the word selected
+ // Regex matches last non-whitespace character
+ let left = inputValue.slice(0, right).search(/\S+$/);
+
+ if (selectionStart === 0) {
+ left = 0;
+ } else if (selectionStart === inputValue.length && left < 0) {
+ left = inputValue.length;
+ } else if (left < 0) {
+ left = selectionStart;
+ }
+
+ return {
+ left,
+ right,
+ };
+ }
}
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
index 886d8113f4a..859d6515531 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -39,6 +39,7 @@
}
this.dismissDropdown();
+ this.dispatchInputEvent();
}
}
@@ -78,7 +79,16 @@
dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open
- this.input.dispatchEvent(new Event('input'));
+ this.input.dispatchEvent(new CustomEvent('input', {
+ bubbles: true,
+ cancelable: true,
+ }));
+ }
+
+ dispatchFormSubmitEvent() {
+ // dispatchEvent() is necessary as form.submit() does not
+ // trigger event handlers
+ this.input.form.dispatchEvent(new Event('submit'));
}
hideDropdown() {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
index 1cd0483877a..00e1c28692f 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -57,28 +57,33 @@
static addWordToInput(tokenName, tokenValue = '') {
const input = document.querySelector('.filtered-search');
+ const inputValue = input.value;
const word = `${tokenName}:${tokenValue}`;
- const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value);
- const lastSearchToken = searchToken.split(' ').last();
- const lastInputCharacter = input.value[input.value.length - 1];
- const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1];
-
- // Remove the typed tokenName
- if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') {
- // Remove spaces after the colon
- if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') {
- input.value = input.value.trim();
- }
-
- input.value = input.value.slice(0, -1 * lastSearchToken.length);
- } else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) {
- // Remove the existing tokenValue
- const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`;
- input.value = input.value.slice(0, -1 * lastTokenString.length);
+ // Get the string to replace
+ let newCaretPosition = input.selectionStart;
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
+
+ input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
+
+ // If we have added a tokenValue at the end of the input,
+ // add a space and set selection to the end
+ if (right >= inputValue.length && tokenValue !== '') {
+ input.value += ' ';
+ newCaretPosition = input.value.length;
}
- input.value += word;
+ gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
+ }
+
+ static updateInputCaretPosition(selectionStart, input) {
+ // Reset the position
+ // Sometimes can end up at end of input
+ input.setSelectionRange(selectionStart, selectionStart);
+
+ const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
+
+ input.setSelectionRange(right, right);
}
updateCurrentDropdownOffset() {
@@ -90,9 +95,18 @@
this.font = window.getComputedStyle(this.filteredSearchInput).font;
}
+ const input = this.filteredSearchInput;
+ const inputText = input.value.slice(0, input.selectionStart);
const filterIconPadding = 27;
- const offset = gl.text
- .getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding;
+ let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
+
+ const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 :
+ this.mapping[key].element.clientWidth;
+ const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
+
+ if (offsetMaxWidth < offset) {
+ offset = offsetMaxWidth;
+ }
this.mapping[key].reference.setOffset(offset);
}
@@ -148,9 +162,9 @@
setDropdown() {
const { lastToken, searchToken } = this.tokenizer
- .processTokens(this.filteredSearchInput.value);
+ .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
- if (this.filteredSearchInput.value.split('').last() === ' ') {
+ if (this.currentDropdown) {
this.updateCurrentDropdownOffset();
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index ffd0d7e9cba..8d62324b79f 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -25,24 +25,32 @@
}
bindEvents() {
+ this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.tokenChange = this.tokenChange.bind(this);
+ this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.addEventListener('click', this.tokenChange);
+ this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
}
unbindEvents() {
+ this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
+ this.filteredSearchInput.removeEventListener('click', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
}
@@ -56,13 +64,26 @@
}
checkForEnter(e) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ const selectionStart = this.filteredSearchInput.selectionStart;
+
+ e.preventDefault();
+ this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+ }
+
if (e.keyCode === 13) {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const dropdownEl = dropdown.element;
+ const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
+
e.preventDefault();
- // Prevent droplab from opening dropdown
- this.dropdownManager.destroyDroplab();
+ if (!activeElements.length) {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
- this.search();
+ this.search();
+ }
}
}
@@ -83,8 +104,14 @@
this.dropdownManager.resetDropdowns();
}
+ handleFormSubmit(e) {
+ e.preventDefault();
+ this.search();
+ }
+
loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray();
+ const usernameParams = this.getUsernameParams();
const inputValues = [];
params.forEach((p) => {
@@ -115,6 +142,16 @@
}
inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ } else if (!match && keyParam === 'assignee_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
+ inputValues.push(`assignee:@${usernameParams[id]}`);
+ }
+ } else if (!match && keyParam === 'author_id') {
+ const id = parseInt(value, 10);
+ if (usernameParams[id]) {
+ inputValues.push(`author:@${usernameParams[id]}`);
+ }
} else if (!match && keyParam === 'search') {
inputValues.push(sanitizedValue);
}
@@ -164,6 +201,27 @@
Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
}
+
+ getUsernameParams() {
+ const usernamesById = {};
+ try {
+ const attribute = this.filteredSearchInput.getAttribute('data-username-params');
+ JSON.parse(attribute).forEach((user) => {
+ usernamesById[user.id] = user.username;
+ });
+ } catch (e) {
+ // do nothing
+ }
+ return usernamesById;
+ }
+
+ tokenChange() {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const currentDropdownRef = dropdown.reference;
+
+ this.setDropdownWrapper();
+ currentDropdownRef.dispatchInputEvent();
+ }
}
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 804d7d9c4ab..249fe23d4cb 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */
(function() {
this.Flash = (function() {
var hideFlash;
@@ -38,7 +38,5 @@
}
return Flash;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 6ca543c2b00..a1b7b442882 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
// Creates the variables for setting up GFM auto-completion
(function() {
@@ -48,8 +48,9 @@
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
- this.setting.highlightFirst = query.length > 0;
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
if (gl.GfmAutoComplete.isLoading(items)) {
+ this.setting.highlightFirst = false;
return items;
}
return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
@@ -153,7 +154,7 @@
return {
username: m.username,
- avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: sanitize(title),
search: sanitize(m.username + " " + m.name)
};
@@ -334,7 +335,7 @@
});
},
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
var match = regexp.exec(subtext);
if (match) {
return match[1];
@@ -371,5 +372,4 @@
return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 00859728c30..7746535d9ed 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,11 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
/* global Turbolinks */
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
- bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
@@ -37,7 +37,7 @@
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault()
+ e.preventDefault();
}
})
.on('input', function() {
@@ -133,7 +133,6 @@
};
return GitLabDropdownFilter;
-
})();
GitLabDropdownRemote = (function() {
@@ -186,7 +185,6 @@
};
return GitLabDropdownRemote;
-
})();
GitLabDropdown = (function() {
@@ -206,7 +204,7 @@
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
- CURSOR_SELECT_SCROLL_PADDING = 5
+ CURSOR_SELECT_SCROLL_PADDING = 5;
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
@@ -223,7 +221,7 @@
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
- this.highlight = !!this.options.highlight
+ this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
: true;
@@ -494,7 +492,7 @@
} else {
var ul = document.createElement('ul');
- for (var i = 0; i < html.length; i++) {
+ for (var i = 0; i < html.length; i += 1) {
var el = html[i];
if (el instanceof jQuery) {
@@ -550,7 +548,7 @@
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
- if (value) { value = value.toString().replace(/'/g, '\\\'') };
+ if (value) { value = value.toString().replace(/'/g, '\\\''); }
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
@@ -641,7 +639,7 @@
: selectedObject.id;
if (isInput) {
field = $(this.el);
- } else if(value) {
+ } else if (value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
@@ -695,8 +693,8 @@
};
GitLabDropdown.prototype.focusTextInput = function() {
- if (this.options.filterable) { this.filterInput.focus() }
- }
+ if (this.options.filterable) { this.filterInput.focus(); }
+ };
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
@@ -802,7 +800,7 @@
listItemBottom = listItemTop + listItemHeight;
if (!index) {
// Scroll the dropdown content to the top
- $dropdownContent.scrollTop(0)
+ $dropdownContent.scrollTop(0);
} else if (index === ($listItems.length - 1)) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
@@ -829,7 +827,6 @@
};
return GitLabDropdown;
-
})();
$.fn.glDropdown = function(opts) {
@@ -839,5 +836,4 @@
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 63f9cafa8d0..16be930a2f4 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
//= require gl_field_error
@@ -45,5 +45,4 @@
}
global.GlFieldErrors = GlFieldErrors;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 04814fa0843..08b2494f3df 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
/* global GitLab */
/* global DropzoneInput */
/* global autosize */
@@ -58,7 +58,5 @@
};
return GLForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
index 3273bf3a263..2e6da5750de 100644
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
@@ -14,7 +14,5 @@
};
return StatGraph;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 2d08a7c6ac3..73715286c4a 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */
/* global ContributorsGraph */
/* global ContributorsAuthorGraph */
/* global ContributorsMasterGraph */
@@ -112,7 +112,5 @@
};
return ContributorsStatGraph;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index 9c5e9381e52..cacfc177fc8 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,11 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, 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, padded-blocks, newline-per-chained-call, no-else-return */
+/* eslint-disable func-names, space-before-function-paren, one-var, 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 */
/* global d3 */
/* global ContributorsGraph */
/*= require d3 */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
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; },
hasProp = {}.hasOwnProperty;
@@ -91,7 +91,6 @@
};
return ContributorsGraph;
-
})();
this.ContributorsMasterGraph = (function(superClass) {
@@ -196,7 +195,6 @@
};
return ContributorsMasterGraph;
-
})(ContributorsGraph);
this.ContributorsAuthorGraph = (function(superClass) {
@@ -274,7 +272,5 @@
};
return ContributorsAuthorGraph;
-
})(ContributorsGraph);
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 1982f4af939..29c3163328f 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, no-plusplus, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
@@ -6,7 +6,7 @@
total = {};
by_author = {};
by_email = {};
- for (i = 0, len = log.length; i < len; i++) {
+ for (i = 0, len = log.length; i < len; i += 1) {
entry = log[i];
if (total[entry.date] == null) {
this.add_date(entry.date, total);
@@ -135,5 +135,4 @@
}
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index 17a76168a79..10dfd05fe3c 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,13 +1,13 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
(function() {
this.GroupAvatar = (function() {
function GroupAvatar() {
- $('.js-choose-group-avatar-button').bind("click", function() {
+ $('.js-choose-group-avatar-button').on("click", function() {
var form;
form = $(this).closest("form");
return form.find(".js-group-avatar-input").click();
});
- $('.js-group-avatar-input').bind("change", function() {
+ $('.js-group-avatar-input').on("change", function() {
var filename, form;
form = $(this).closest("form");
filename = $(this).val().replace(/^.*[\\\/]/, '');
@@ -16,7 +16,5 @@
}
return GroupAvatar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
index 8e10e424412..15e695e81cf 100644
--- a/app/assets/javascripts/group_label_subscription.js.es6
+++ b/app/assets/javascripts/group_label_subscription.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
(function(global) {
class GroupLabelSubscription {
@@ -50,5 +50,4 @@
}
global.GroupLabelSubscription = GroupLabelSubscription;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 99700e7562a..a50bc4a9057 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */
/* global Api */
(function() {
@@ -67,7 +67,5 @@
};
return GroupsSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index c7cbf9ca44b..fa85f9a6c86 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,10 +1,8 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, padded-blocks, prefer-arrow-callback, no-var, max-len */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */
(function() {
-
$(document).on('todo:toggle', function(e, count) {
var $todoPendingCount = $('.todos-pending-count');
$todoPendingCount.text(gl.text.addDelimiter(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
-
})();
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index fa795be07ed..9390136d3d8 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
(function() {
window.ImporterStatus = (function() {
@@ -68,7 +68,6 @@
};
return ImporterStatus;
-
})();
$(function() {
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 9c3c96c20ed..f63d700fd65 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
+/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global Issuable */
/* global Turbolinks */
@@ -34,7 +34,6 @@
e.preventDefault();
debouncedExecSearch(e);
});
-
},
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
@@ -152,7 +151,7 @@
this.issuableBulkActions.setOriginalDropdownData();
if ($checkedIssues.length > 0) {
- let ids = $.map($checkedIssues, function(value) {
+ const ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
});
$updateIssuesIds.val(ids);
@@ -187,5 +186,4 @@
});
}
};
-
})(window);
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6
new file mode 100644
index 00000000000..7d0465aa8b4
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6
@@ -0,0 +1 @@
+//= require ./time_tracking/time_tracking_bundle
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
new file mode 100644
index 00000000000..72433df2818
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
@@ -0,0 +1,42 @@
+/* global Vue */
+//= require lib/utils/pretty_time
+
+(() => {
+ Vue.component('time-tracking-collapsed-state', {
+ name: 'time-tracking-collapsed-state',
+ props: [
+ 'showComparisonState',
+ 'showSpentOnlyState',
+ 'showEstimateOnlyState',
+ 'showNoTimeTrackingState',
+ 'timeSpentHumanReadable',
+ 'timeEstimateHumanReadable',
+ 'stopwatchSvg',
+ ],
+ methods: {
+ abbreviateTime(timeStr) {
+ return gl.utils.prettyTime.abbreviateTime(timeStr);
+ },
+ },
+ template: `
+ <div class='sidebar-collapsed-icon'>
+ <div v-html='stopwatchSvg'></div>
+ <div class='time-tracking-collapsed-summary'>
+ <div class='compare' v-if='showComparisonState'>
+ <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='estimate-only' v-if='showEstimateOnlyState'>
+ <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
+ </div>
+ <div class='spend-only' v-if='showSpentOnlyState'>
+ <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
+ </div>
+ <div class='no-tracking' v-if='showNoTimeTrackingState'>
+ <span class='no-value'>None</span>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
+
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
new file mode 100644
index 00000000000..6abbd5dd167
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
@@ -0,0 +1,69 @@
+/* global Vue */
+//= require lib/utils/pretty_time
+
+(() => {
+ const prettyTime = gl.utils.prettyTime;
+
+ Vue.component('time-tracking-comparison-pane', {
+ name: 'time-tracking-comparison-pane',
+ props: [
+ 'timeSpent',
+ 'timeEstimate',
+ 'timeSpentHumanReadable',
+ 'timeEstimateHumanReadable',
+ ],
+ computed: {
+ parsedRemaining() {
+ const diffSeconds = this.timeEstimate - this.timeSpent;
+ return prettyTime.parseSeconds(diffSeconds);
+ },
+ timeRemainingHumanReadable() {
+ return prettyTime.stringifyTime(this.parsedRemaining);
+ },
+ timeRemainingTooltip() {
+ const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
+ return `${prefix} ${this.timeRemainingHumanReadable}`;
+ },
+ /* Diff values for comparison meter */
+ timeRemainingMinutes() {
+ return this.timeEstimate - this.timeSpent;
+ },
+ timeRemainingPercent() {
+ return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
+ },
+ timeRemainingStatusClass() {
+ return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
+ },
+ /* Parsed time values */
+ parsedEstimate() {
+ return prettyTime.parseSeconds(this.timeEstimate);
+ },
+ parsedSpent() {
+ return prettyTime.parseSeconds(this.timeSpent);
+ },
+ },
+ template: `
+ <div class='time-tracking-comparison-pane'>
+ <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
+ :aria-valuenow='timeRemainingTooltip'
+ :title='timeRemainingTooltip'
+ :data-original-title='timeRemainingTooltip'
+ :class='timeRemainingStatusClass'>
+ <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
+ <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
+ </div>
+ <div class='compare-display-container'>
+ <div class='compare-display pull-left'>
+ <span class='compare-label'>Spent</span>
+ <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
+ </div>
+ <div class='compare-display estimated pull-right'>
+ <span class='compare-label'>Est</span>
+ <span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
new file mode 100644
index 00000000000..309e9f2f9ef
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
@@ -0,0 +1,13 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-estimate-only-pane', {
+ name: 'time-tracking-estimate-only-pane',
+ props: ['timeEstimateHumanReadable'],
+ template: `
+ <div class='time-tracking-estimate-only-pane'>
+ <span class='bold'>Estimated:</span>
+ {{ timeEstimateHumanReadable }}
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
new file mode 100644
index 00000000000..d7ced6d7151
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
@@ -0,0 +1,24 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-help-state', {
+ name: 'time-tracking-help-state',
+ props: ['docsUrl'],
+ template: `
+ <div class='time-tracking-help-state'>
+ <div class='time-tracking-info'>
+ <h4>Track time with slash commands</h4>
+ <p>Slash commands can be used in the issues description and comment boxes.</p>
+ <p>
+ <code>/estimate</code>
+ will update the estimated time with the latest command.
+ </p>
+ <p>
+ <code>/spend</code>
+ will update the sum of the time spent.
+ </p>
+ <a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
new file mode 100644
index 00000000000..1d2ca643b5b
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
@@ -0,0 +1,11 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-no-tracking-pane', {
+ 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/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
new file mode 100644
index 00000000000..ed283fec3c3
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
@@ -0,0 +1,13 @@
+/* global Vue */
+(() => {
+ Vue.component('time-tracking-spent-only-pane', {
+ name: 'time-tracking-spent-only-pane',
+ props: ['timeSpentHumanReadable'],
+ template: `
+ <div class='time-tracking-spend-only-pane'>
+ <span class='bold'>Spent:</span>
+ {{ timeSpentHumanReadable }}
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
new file mode 100644
index 00000000000..26563a7713b
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
@@ -0,0 +1,118 @@
+/* global Vue */
+//= require ./help_state
+//= require ./collapsed_state
+//= require ./spent_only_pane
+//= require ./no_tracking_pane
+//= require ./estimate_only_pane
+//= require ./comparison_pane
+
+(() => {
+ Vue.component('issuable-time-tracker', {
+ name: 'issuable-time-tracker',
+ props: [
+ 'time_estimate',
+ 'time_spent',
+ 'human_time_estimate',
+ 'human_time_spent',
+ 'stopwatchSvg',
+ 'docsUrl',
+ ],
+ data() {
+ return {
+ showHelp: false,
+ };
+ },
+ computed: {
+ timeSpent() {
+ return this.time_spent;
+ },
+ timeEstimate() {
+ return this.time_estimate;
+ },
+ timeEstimateHumanReadable() {
+ return this.human_time_estimate;
+ },
+ timeSpentHumanReadable() {
+ return this.human_time_spent;
+ },
+ hasTimeSpent() {
+ return !!this.timeSpent;
+ },
+ hasTimeEstimate() {
+ return !!this.timeEstimate;
+ },
+ showComparisonState() {
+ return this.hasTimeEstimate && this.hasTimeSpent;
+ },
+ showEstimateOnlyState() {
+ return this.hasTimeEstimate && !this.hasTimeSpent;
+ },
+ showSpentOnlyState() {
+ return this.hasTimeSpent && !this.hasTimeEstimate;
+ },
+ showNoTimeTrackingState() {
+ return !this.hasTimeEstimate && !this.hasTimeSpent;
+ },
+ showHelpState() {
+ return !!this.showHelp;
+ },
+ },
+ methods: {
+ toggleHelpState(show) {
+ this.showHelp = show;
+ },
+ },
+ template: `
+ <div class='time_tracker time-tracking-component-wrap' v-cloak>
+ <time-tracking-collapsed-state
+ :show-comparison-state='showComparisonState'
+ :show-help-state='showHelpState'
+ :show-spent-only-state='showSpentOnlyState'
+ :show-estimate-only-state='showEstimateOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'
+ :stopwatch-svg='stopwatchSvg'>
+ </time-tracking-collapsed-state>
+ <div class='title hide-collapsed'>
+ Time tracking
+ <div class='help-button pull-right'
+ v-if='!showHelpState'
+ @click='toggleHelpState(true)'>
+ <i class='fa fa-question-circle'></i>
+ </div>
+ <div class='close-help-button pull-right'
+ v-if='showHelpState'
+ @click='toggleHelpState(false)'>
+ <i class='fa fa-close'></i>
+ </div>
+ </div>
+ <div class='time-tracking-content hide-collapsed'>
+ <time-tracking-estimate-only-pane
+ v-if='showEstimateOnlyState'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-estimate-only-pane>
+ <time-tracking-spent-only-pane
+ v-if='showSpentOnlyState'
+ :time-spent-human-readable='timeSpentHumanReadable'>
+ </time-tracking-spent-only-pane>
+ <time-tracking-no-tracking-pane
+ v-if='showNoTimeTrackingState'>
+ </time-tracking-no-tracking-pane>
+ <time-tracking-comparison-pane
+ v-if='showComparisonState'
+ :time-estimate='timeEstimate'
+ :time-spent='timeSpent'
+ :time-spent-human-readable='timeSpentHumanReadable'
+ :time-estimate-human-readable='timeEstimateHumanReadable'>
+ </time-tracking-comparison-pane>
+ <transition name='help-state-toggle'>
+ <time-tracking-help-state
+ v-if='showHelpState'
+ :docs-url='docsUrl'>
+ </time-tracking-help-state>
+ </transition>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
new file mode 100644
index 00000000000..0b8da2b1f4f
--- /dev/null
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
@@ -0,0 +1,61 @@
+/* global Vue */
+//= require ./components/time_tracker
+//= require smart_interval
+//= require subbable_resource
+
+(() => {
+ /* This Vue instance represents what will become the parent instance for the
+ * sidebar. It will be responsible for managing `issuable` state and propagating
+ * changes to sidebar components. We will want to create a separate service to
+ * interface with the server at that point.
+ */
+
+ class IssuableTimeTracking {
+ constructor(issuableJSON) {
+ const parsedIssuable = JSON.parse(issuableJSON);
+ return this.initComponent(parsedIssuable);
+ }
+
+ initComponent(parsedIssuable) {
+ this.parentInstance = new Vue({
+ el: '#issuable-time-tracker',
+ data: {
+ issuable: parsedIssuable,
+ },
+ methods: {
+ fetchIssuable() {
+ return gl.IssuableResource.get.call(gl.IssuableResource, {
+ type: 'GET',
+ url: gl.IssuableResource.endpoint,
+ });
+ },
+ updateState(data) {
+ this.issuable = data;
+ },
+ subscribeToUpdates() {
+ gl.IssuableResource.subscribe(data => this.updateState(data));
+ },
+ listenForSlashCommands() {
+ $(document).on('ajax:success', '.gfm-form', (e, data) => {
+ const subscribedCommands = ['spend_time', 'time_estimate'];
+ const changedCommands = data.commands_changes;
+
+ if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
+ this.fetchIssuable();
+ }
+ });
+ },
+ },
+ created() {
+ this.fetchIssuable();
+ },
+ mounted() {
+ this.subscribeToUpdates();
+ this.listenForSlashCommands();
+ },
+ });
+ }
+ }
+
+ gl.IssuableTimeTracking = IssuableTimeTracking;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 4aaad111082..9c53cdee58e 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
/* global UsersSelect */
(function() {
@@ -66,7 +66,5 @@
};
return IssuableContext;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 1c4086517fe..293b856dc4d 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,11 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.IssuableForm = (function() {
IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
@@ -51,7 +51,7 @@
IssuableForm.prototype.handleSubmit = function() {
var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
- if ((parseInt(fieldId) || 0) > 0) {
+ if ((parseInt(fieldId, 10) || 0) > 0) {
if (!confirm(this.issueMoveConfirmMsg)) {
return false;
}
@@ -150,7 +150,5 @@
};
return IssuableForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 61e8531153b..081b0d8b0d7 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
/*= require flash */
@@ -6,7 +6,7 @@
/*= require task_list */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Issue = (function() {
function Issue() {
@@ -151,7 +151,5 @@
};
return Issue;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index b39d8274e13..1d6eff11403 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
@@ -30,7 +30,5 @@
}
return IssueStatusSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 52fd5d71b18..e0ebd36a65c 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,9 +1,8 @@
-/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
+/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global Issuable */
/* global Flash */
((global) => {
-
class IssuableBulkActions {
constructor({ container, form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
@@ -62,7 +61,6 @@
return labels;
}
-
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
@@ -81,7 +79,6 @@
return result;
}
-
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
@@ -163,5 +160,4 @@
}
global.IssuableBulkActions = IssuableBulkActions;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 33c5e35324d..8f48b1f57ce 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,8 +1,7 @@
-/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */
+/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* global Flash */
((global) => {
-
class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
@@ -104,5 +103,4 @@
}
gl.LabelManager = LabelManager;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 10de13c9a8a..40ad6fc348e 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,6 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Labels = (function() {
function Labels() {
@@ -42,7 +42,5 @@
};
return Labels;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index ec2fc87bece..fd1e229e30a 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */
+/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */
/* global Issuable */
/* global ListLabel */
@@ -333,7 +333,7 @@
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown.parent()
.find('.dropdown-clear-active')
- .removeClass('is-active')
+ .removeClass('is-active');
}
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
@@ -484,5 +484,4 @@
return LabelsSelect;
})();
-
}).call(this);
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 2b700539c2b..1c0ea317c1a 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, indent, vars-on-top, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
+
(function() {
var hideEndFade;
@@ -27,10 +28,10 @@
});
$scrollingTabs.each(function () {
- var $this = $(this),
- scrollingTabWidth = $this.width(),
- $active = $this.find('.active'),
- activeWidth = $active.width();
+ var $this = $(this);
+ var scrollingTabWidth = $this.width();
+ var $active = $this.find('.active');
+ var activeWidth = $active.width();
if ($active.length) {
var offset = $active.offset().left + activeWidth;
@@ -43,5 +44,4 @@
}
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index 83957af94f3..ce090a2e4fd 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */
(function() {
(function(w) {
if (w.gl == null) {
@@ -46,5 +46,4 @@
return dfd.promise();
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 0c6a3cc3170..7452879d9a3 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len, prefer-template */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */
(function() {
(function(w) {
var base;
@@ -160,6 +160,18 @@
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
- })(window);
+ /**
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeHeaders = (headers) => {
+ const upperCaseHeaders = {};
+ Object.keys(headers).forEach((e) => {
+ upperCaseHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ return upperCaseHeaders;
+ };
+ })(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index e8e502694d6..3ed8bfd5651 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
/* global timeago */
/* global dateFormat */
@@ -97,7 +97,5 @@
return Math.floor((date2 - date1) / millisecondsPerDay);
};
-
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 3c9ad0e67c8..6d5979603b9 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */
(function() {
(function(w) {
@@ -44,5 +44,4 @@
w.notify = notifyMe;
return w.notifyPermissions = notifyPermissions;
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6
index ccaf447eb0b..ae397212e55 100644
--- a/app/assets/javascripts/lib/utils/pretty_time.js.es6
+++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6
@@ -4,13 +4,13 @@
* stringifyTime condensed or non-condensed, abbreviateTimelengths)
* */
- class PrettyTime {
-
+ const utils = window.gl.utils = gl.utils || {};
+ const prettyTime = utils.prettyTime = {
/*
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
* Seconds can be negative or positive, zero or non-zero.
*/
- static parseSeconds(seconds) {
+ parseSeconds(seconds) {
const DAYS_PER_WEEK = 5;
const HOURS_PER_DAY = 8;
const MINUTES_PER_HOUR = 60;
@@ -24,7 +24,7 @@
minutes: 1,
};
- let unorderedMinutes = PrettyTime.secondsToMinutes(seconds);
+ let unorderedMinutes = prettyTime.secondsToMinutes(seconds);
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
@@ -33,35 +33,33 @@
return periodCount;
});
- }
+ },
/*
* Accepts a timeObject and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/
- static stringifyTime(timeObject) {
+ stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
}, '').trim();
return reducedTime.length ? reducedTime : '0m';
- }
+ },
/*
* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
* the first non-zero unit/value pair.
*/
- static abbreviateTime(timeStr) {
+ abbreviateTime(timeStr) {
return timeStr.split(' ')
.filter(unitStr => unitStr.charAt(0) !== '0')[0];
- }
+ },
- static secondsToMinutes(seconds) {
+ secondsToMinutes(seconds) {
return Math.abs(seconds / 60);
- }
- }
-
- gl.PrettyTime = PrettyTime;
+ },
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index c856a26ae40..6bb575059b7 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, semi, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */
+
(function() {
(function(w) {
var base;
@@ -10,7 +11,7 @@
}
gl.text.addDelimiter = function(text) {
return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
- }
+ };
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
@@ -159,10 +160,9 @@
};
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
- }
+ };
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index 961859dfb5b..6d813d61601 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, max-len */
(function() {
(function(w) {
var base;
@@ -12,5 +12,4 @@
return (obj != null) && (obj.constructor === Object);
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 6872186cd7f..8e15bf0735c 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, no-plusplus, guard-for-in, no-restricted-syntax, prefer-template, quotes, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
(function() {
(function(w) {
var base;
@@ -22,7 +22,7 @@
if (sParameterName[0] === sParam) {
values.push(sParameterName[1].replace(/\+/g, ' '));
}
- i++;
+ i += 1;
}
return values;
};
@@ -57,7 +57,7 @@
return ((function() {
var j, len, results;
results = [];
- for (j = 0, len = urlVariables.length; j < len; j++) {
+ for (j = 0, len = urlVariables.length; j < len; j += 1) {
variables = urlVariables[j];
if (variables.indexOf(param) === -1) {
results.push(variables);
@@ -77,5 +77,4 @@
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
})(window);
-
}).call(this);
diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6
new file mode 100644
index 00000000000..eff1dcabfa2
--- /dev/null
+++ b/app/assets/javascripts/lib/vue_resource.js.es6
@@ -0,0 +1,2 @@
+//= require vue
+//= require vue-resource
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 9af89b79f84..4620715a521 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
// LineHighlighter
//
@@ -31,7 +31,7 @@
// </div>
//
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.LineHighlighter = (function() {
// CSS class applied to highlighted lines
@@ -119,11 +119,11 @@
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
- //?L(\d+)(?:-(\d+))?$/)
+ // ?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
- first = parseInt(matches[1]);
- last = matches[2] ? parseInt(matches[2]) : null;
+ first = parseInt(matches[1], 10);
+ last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
@@ -144,7 +144,7 @@
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
- for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? ++i : --i) {
+ for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
results.push(this.highlightLine(lineNumber));
}
return results;
@@ -178,7 +178,5 @@
};
return LineHighlighter;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 0ae6df311bb..ea9bfb4860a 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
/* global Turbolinks */
(function() {
@@ -11,5 +11,4 @@
$(document).on('page:change', function() {
$('.tanuki-logo').removeClass('animate');
});
-
}).call(this);
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index f95b079c972..c7e78fed8fe 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -1,10 +1,9 @@
-/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
/* global Vue */
/* global ace */
/* global Flash */
((global) => {
-
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.diffFileEditor = Vue.extend({
@@ -19,7 +18,7 @@
loading: false,
fileLoaded: false,
originalContent: '',
- }
+ };
},
computed: {
classObject() {
@@ -51,8 +50,8 @@
$.get(this.file.content_path)
.done((file) => {
- let content = this.$el.querySelector('pre');
- let fileContent = document.createTextNode(file.content);
+ const content = this.$el.querySelector('pre');
+ const fileContent = document.createTextNode(file.content);
content.textContent = fileContent.textContent;
@@ -94,5 +93,4 @@
}
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
index 74544b7d0c7..240c8f98932 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -1,8 +1,7 @@
-/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign, comma-dangle */
/* global Vue */
((global) => {
-
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.inlineConflictLines = Vue.extend({
@@ -11,5 +10,4 @@
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
index 78c00c31c16..97753c50b60 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -1,8 +1,7 @@
-/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* eslint-disable no-param-reassign, comma-dangle */
/* global Vue */
((global) => {
-
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLines = Vue.extend({
@@ -26,5 +25,4 @@
</table>
`,
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
index 8df3170edac..c012b77e0bf 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */
+/* eslint-disable no-param-reassign, comma-dangle */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -25,8 +25,7 @@
method: 'POST'
});
}
- };
+ }
global.mergeConflicts.mergeConflictsService = mergeConflictsService;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
index 53b44007510..74587df22c5 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */
+/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */
/* global Cookies */
/* global Vue */
@@ -40,7 +40,6 @@
commitMessage: data.commit_message,
sourceBranch: data.source_branch,
targetBranch: data.target_branch,
- commitMessage: data.commit_message,
shortCommitSha: data.commit_sha.slice(0, 7),
};
},
@@ -89,7 +88,7 @@
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
- })
+ });
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
@@ -121,7 +120,7 @@
} else {
const lineType = type || 'context';
- linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+ linesObj.left.push(this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
@@ -129,7 +128,7 @@
this.checkLineLengths(linesObj);
});
- for (let i = 0, len = linesObj.left.length; i < len; i++) {
+ for (let i = 0, len = linesObj.left.length; i < len; i += 1) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
@@ -162,11 +161,11 @@
if (file.type === CONFLICT_TYPES.TEXT) {
file.sections.forEach((section) => {
if (section.conflict) {
- count++;
+ count += 1;
}
});
} else {
- count++;
+ count += 1;
}
});
@@ -252,17 +251,17 @@
},
checkLineLengths(linesObj) {
- let { left, right } = linesObj;
+ const { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
- for (let i = 0; i < diff; i++) {
+ for (let i = 0; i < diff; i += 1) {
right.push({ lineType: 'emptyLine', richText: '' });
}
} else {
const diff = right.length - left.length;
- for (let i = 0; i < diff; i++) {
+ for (let i = 0; i < diff; i += 1) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
@@ -316,32 +315,31 @@
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
- for (let i = 0, l = files.length; i < l; i++) {
- let file = files[i];
+ for (let i = 0, l = files.length; i < l; i += 1) {
+ const file = files[i];
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
let numberConflicts = 0;
- let resolvedConflicts = Object.keys(file.resolutionData).length
+ const resolvedConflicts = Object.keys(file.resolutionData).length;
// We only check for conflicts type 'text'
// since conflicts `text_editor` can´t be resolved in interactive mode
if (file.type === CONFLICT_TYPES.TEXT) {
- for (let j = 0, k = file.sections.length; j < k; j++) {
+ for (let j = 0, k = file.sections.length; j < k; j += 1) {
if (file.sections[j].conflict) {
- numberConflicts++;
+ numberConflicts += 1;
}
}
if (resolvedConflicts !== numberConflicts) {
- unresolved++;
+ unresolved += 1;
}
}
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
-
// Unlikely to happen since switching to Edit mode saves content automatically.
// Checking anyway in case the save strategy changes in the future
if (!file.content) {
- unresolved++;
+ unresolved += 1;
continue;
}
}
@@ -366,15 +364,12 @@
};
this.state.conflictsData.files.forEach((file) => {
- let addFile;
-
- addFile = {
+ const addFile = {
old_path: file.old_path,
new_path: file.new_path
};
if (file.type === CONFLICT_TYPES.TEXT) {
-
// Submit only one data for type of editing
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
addFile.sections = file.resolutionData;
@@ -435,5 +430,4 @@
return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT);
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
index 83520702f9b..a2d90f9ba47 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable new-cap, comma-dangle, no-new, semi */
+/* eslint-disable new-cap, comma-dangle, no-new */
/* global Vue */
/* global Flash */
@@ -29,10 +29,10 @@ $(() => {
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
},
computed: {
- conflictsCountText() { return mergeConflictsStore.getConflictsCountText() },
- readyToCommit() { return mergeConflictsStore.isReadyToCommit() },
- commitButtonText() { return mergeConflictsStore.getCommitButtonText() },
- showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() }
+ conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
+ readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
+ commitButtonText() { return mergeConflictsStore.getCommitButtonText(); },
+ showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
},
created() {
mergeConflictsService
@@ -88,5 +88,5 @@ $(() => {
});
}
}
- })
+ });
});
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
index e89b35d5407..53e000d7e9e 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */
+/* eslint-disable no-param-reassign, comma-dangle */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -10,5 +10,4 @@
}
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
index a4aca85d460..0f475f62ee6 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */
+/* eslint-disable no-param-reassign, quote-props, comma-dangle */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
@@ -16,5 +16,4 @@
}
}
};
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 244c2f6746c..09ee8dbe9d7 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
/* global MergeRequestTabs */
/*= require jquery.waitforimages */
@@ -6,7 +6,7 @@
/*= require merge_request_tabs */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MergeRequest = (function() {
function MergeRequest(opts) {
@@ -130,7 +130,5 @@
};
return MergeRequest;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 0305aeb07d9..7cc319e2f4e 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,11 +1,11 @@
-/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */
+/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */
/* global notify */
/* global notifyPermissions */
/* global merge_request_widget */
/* global Turbolinks */
((global) => {
- var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
<div class="ci_widget ci-success">
@@ -90,7 +90,7 @@
const $ciSuccessIcon = $('.js-success-icon');
this.$ciSuccessIcon = $ciSuccessIcon.html();
$ciSuccessIcon.remove();
- }
+ };
MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
if (deleteSourceBranch == null) {
@@ -126,7 +126,9 @@
MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) {
- return $('.mr-state-widget').replaceWith(data);
+ var $html = $(data);
+ $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
+ $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
});
};
@@ -187,9 +189,9 @@
};
MergeRequestWidget.prototype.renderEnvironments = function(environments) {
- for (let i = 0; i < environments.length; i++) {
+ for (let i = 0; i < environments.length; i += 1) {
const environment = environments[i];
- if ($(`.mr-state-widget #${ environment.id }`).length) return;
+ if ($(`.mr-state-widget #${environment.id}`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
@@ -205,7 +207,7 @@
}
environment.ci_success_icon = this.$ciSuccessIcon;
const templateString = _.unescape($template[0].outerHTML);
- const template = _.template(templateString)(environment)
+ const template = _.template(templateString)(environment);
this.$widgetBody.before(template);
}
};
@@ -247,7 +249,5 @@
};
return MergeRequestWidget;
-
})();
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
index 2b074994b4a..5969d2ba56b 100644
--- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
+++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
@@ -8,31 +8,42 @@
* temporarily.
* */
- if ($('.accept-mr-form').length) {
- $('.accept-mr-form').on('ajax:send', () => {
- $('.accept-mr-form :input').disable();
- });
+ $(document)
+ .off('ajax:send', '.accept-mr-form')
+ .on('ajax:send', '.accept-mr-form', () => {
+ $('.accept-mr-form :input').disable();
+ });
- $('.accept_merge_request').on('click', () => {
- $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
- });
+ $(document)
+ .off('click', '.accept_merge_request')
+ .on('click', '.accept_merge_request', () => {
+ $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
+ });
- $('.merge_when_build_succeeds').on('click', () => {
- $('#merge_when_build_succeeds').val('1');
- });
+ $(document)
+ .off('click', '.merge_when_build_succeeds')
+ .on('click', '.merge_when_build_succeeds', () => {
+ $('#merge_when_build_succeeds').val('1');
+ });
- $('.js-merge-dropdown a').on('click', (e) => {
- e.preventDefault();
- $(this).closest('form').submit();
- });
- } else if ($('.rebase-in-progress').length) {
+ $(document)
+ .off('click', '.js-merge-dropdown a')
+ .on('click', '.js-merge-dropdown a', (e) => {
+ e.preventDefault();
+ $(e.target).closest('form').submit();
+ });
+ if ($('.rebase-in-progress').length) {
merge_request_widget.rebaseInProgress();
} else if ($('.rebase-mr-form').length) {
- $('.rebase-mr-form').on('ajax:send', () => {
+ $(document)
+ .off('ajax:send', '.rebase-mr-form')
+ .on('ajax:send', '.rebase-mr-form', () => {
$('.rebase-mr-form :input').disable();
});
- $('.js-rebase-button').on('click', () => {
+ $(document)
+ .off('click', '.js-rebase-button')
+ .on('click', '.js-rebase-button', () => {
$('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
});
} else {
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 9f8af46c715..527cdc9b698 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MergedButtons = (function() {
function MergedButtons() {
@@ -41,7 +41,5 @@
};
return MergedButtons;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 42152362e60..7ce1259e015 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */
(function() {
@@ -193,7 +193,5 @@
};
return Milestone;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 28054b78249..7ab39ffbd05 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Vue */
/* global Issuable */
/* global ListMilestone */
@@ -181,7 +181,5 @@
}
return MilestoneSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
index 90b3366f14b..80549532ea9 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -10,9 +10,9 @@
* The container should be the table element.
*
* The stage icon clicked needs to have the following HTML structure:
- * <div>
- * <button class="dropdown js-builds-dropdown-button"></button>
- * <div class="js-builds-dropdown-container"></div>
+ * <div class="dropdown">
+ * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button>
+ * <div class="js-builds-dropdown-container dropdown-menu"></div>
* </div>
*/
(() => {
@@ -26,13 +26,11 @@
}
/**
- * Adds and removes the event listener.
+ * Adds the event listener when the dropdown is opened.
+ * All dropdown events are fired at the .dropdown-menu's parent element.
*/
bindEvents() {
- const dropdownButtonSelector = 'button.js-builds-dropdown-button';
-
- $(this.container).off('click', dropdownButtonSelector, this.getBuildsList)
- .on('click', dropdownButtonSelector, this.getBuildsList);
+ $(this.container).on('shown.bs.dropdown', this.getBuildsList);
}
/**
@@ -52,11 +50,14 @@
/**
* For the clicked stage, gets the list of builds.
*
- * @param {Object} e
+ * All dropdown events have a relatedTarget property,
+ * whose value is the toggling anchor element.
+ *
+ * @param {Object} e bootstrap dropdown event
* @return {Promise}
*/
getBuildsList(e) {
- const button = e.currentTarget;
+ const button = e.relatedTarget;
const endpoint = button.dataset.stageEndpoint;
return $.ajax({
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 6633f2c2709..514556ade0b 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */
/* global Api */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
window.NamespaceSelect = (function() {
function NamespaceSelect(opts) {
@@ -63,7 +63,6 @@
};
return NamespaceSelect;
-
})();
window.NamespaceSelects = (function() {
@@ -83,7 +82,5 @@
}
return NamespaceSelects;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 20a68780cd5..a7ccd03b60c 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
/* global Raphael */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.BranchGraph = (function() {
function BranchGraph(element1, options1) {
@@ -53,7 +53,7 @@
this.top = this.r.set();
this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
ref = this.commits;
- for (j = 0, len = ref.length; j < len; j++) {
+ for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
if (c.id in this.parents) {
c.isParent = true;
@@ -68,7 +68,7 @@
var c, j, len, p, ref, results;
ref = this.commits;
results = [];
- for (j = 0, len = ref.length; j < len; j++) {
+ for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
this.mtime = Math.max(this.mtime, c.time);
this.mspace = Math.max(this.mspace, c.space);
@@ -76,7 +76,7 @@
var l, len1, ref1, results1;
ref1 = c.parents;
results1 = [];
- for (l = 0, len1 = ref1.length; l < len1; l++) {
+ for (l = 0, len1 = ref1.length; l < len1; l += 1) {
p = ref1[l];
this.parents[p[0]] = true;
results1.push(this.mspace = Math.max(this.mspace, p[1]));
@@ -96,7 +96,7 @@
// Skipping a few colors in the spectrum to get more contrast between colors
Raphael.getColor();
Raphael.getColor();
- results.push(k++);
+ results.push(k += 1);
}
return results;
};
@@ -113,7 +113,7 @@
fill: "#444"
});
ref = this.days;
- for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
+ for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@@ -286,7 +286,7 @@
r = this.r;
ref = commit.parents;
results = [];
- for (i = j = 0, len = ref.length; j < len; i = ++j) {
+ for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
parent = ref[i];
parentCommit = this.preparedCommits[parent[0]];
parentY = this.offsetY + this.unitTime * parentCommit.time;
@@ -346,7 +346,6 @@
};
return BranchGraph;
-
})();
Raphael.prototype.commitTooltip = function(x, y, commit) {
@@ -399,7 +398,7 @@
words = content.split(" ");
x = 0;
s = [];
- for (j = 0, len = words.length; j < len; j++) {
+ for (j = 0, len = words.length; j < len; j += 1) {
word = words[j];
if (x + (word.length * letterWidth) > width) {
s.push("\n");
@@ -422,5 +421,4 @@
y: h
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 2367d2497b2..37bf6436fd1 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
/* global BranchGraph */
(function() {
@@ -16,7 +16,5 @@
}
return Network;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 17833d3419a..2e6eb83cec7 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
/* global Network */
/* global ShortcutsNetwork */
@@ -23,5 +23,4 @@
});
return new ShortcutsNetwork(network_graph.branch_graph);
});
-
}).call(this);
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 29a323dd4c6..7f763c13b50 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
this.NewBranchForm = (function() {
function NewBranchForm(form, availableRefs) {
@@ -99,7 +99,5 @@
};
return NewBranchForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 8fb8f3e4a5f..41eea78a3e6 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,6 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NewCommitForm = (function() {
function NewCommitForm(form) {
@@ -29,7 +29,5 @@
};
return NewCommitForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index fac21f8cd32..9db830a7ada 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
/* global Flash */
/* global GLForm */
/* global Autosave */
@@ -14,7 +14,7 @@
/*= require task_list */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Notes = (function() {
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -220,7 +220,6 @@
})(this));
};
-
/*
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
@@ -244,7 +243,6 @@
return this.initRefresh();
};
-
Notes.prototype.handleCreateChanges = function(note) {
if (typeof note === 'undefined') {
return;
@@ -294,7 +292,6 @@
}
};
-
/*
Check if note does not exists on page
*/
@@ -307,7 +304,6 @@
return this.view === 'parallel';
};
-
/*
Render note in discussion area.
@@ -358,7 +354,6 @@
return this.updateNotesCount(1);
};
-
/*
Called in response the main target form has been successfully submitted.
@@ -390,7 +385,6 @@
return form.find(".js-note-text").trigger("input");
};
-
/*
Shows the main form and does some setup on it.
@@ -415,7 +409,6 @@
return this.parentTimeline = form.parents('.timeline');
};
-
/*
General note form setup.
@@ -432,7 +425,6 @@
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
-
/*
Called in response to the new note form being submitted
@@ -448,7 +440,6 @@
return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
};
-
/*
Called in response to the new note form being submitted
@@ -473,7 +464,6 @@
this.removeDiscussionNoteForm($form);
};
-
/*
Called in response to the edit note form being submitted
@@ -498,7 +488,6 @@
}
};
-
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var currentContent = $el.find('.note-textarea').val();
@@ -520,8 +509,7 @@
}
return isAllowed;
- }
-
+ };
/*
Called in response to clicking the edit note link
@@ -551,7 +539,6 @@
this.putEditFormInPlace($target);
};
-
/*
Called in response to clicking the edit note link
@@ -596,7 +583,6 @@
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
-
/*
Called in response to deleting a note of any kind.
@@ -636,7 +622,6 @@
return this.updateNotesCount(-1);
};
-
/*
Called in response to clicking the delete attachment link
@@ -653,7 +638,6 @@
return note.find(".current-note-edit-form").remove();
};
-
/*
Called when clicking on the "reply" button for a diff line.
@@ -673,7 +657,6 @@
return this.setupDiscussionNoteForm(replyLink, form);
};
-
/*
Shows the diff or discussion form and does some setup on it.
@@ -715,7 +698,6 @@
.addClass("discussion-form js-discussion-note-form");
};
-
/*
Called when clicking on the "add a comment" button on the side of a diff line.
@@ -772,7 +754,6 @@
}
};
-
/*
Called in response to "cancel" on a diff note form.
@@ -806,7 +787,6 @@
return this.removeDiscussionNoteForm(form);
};
-
/*
Called after an attachment file has been selected.
@@ -821,7 +801,6 @@
return form.find(".js-attachment-filename").text(filename);
};
-
/*
Called when the tab visibility changes
*/
@@ -915,7 +894,7 @@
$editForm.find('.js-note-text').focus().val(originalContent);
$editForm.find('.js-md-write-button').trigger('click');
$editForm.find('.referenced-users').hide();
- }
+ };
Notes.prototype.updateTaskList = function(e) {
var $target = $(e.target);
@@ -929,7 +908,7 @@
};
Notes.prototype.updateNotesCount = function(updateCount) {
- return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
};
Notes.prototype.resolveDiscussion = function() {
@@ -974,7 +953,5 @@
};
return Notes;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 5d0d594073d..926dc35fee8 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */
/* global Flash */
(function() {
@@ -27,7 +27,5 @@
}
return NotificationsDropdown;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 2034f9a748a..c3d7cc0adfb 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,6 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.NotificationsForm = (function() {
function NotificationsForm() {
@@ -53,7 +53,5 @@
};
return NotificationsForm;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index 0b09ad113a3..43263368494 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,12 +1,10 @@
-/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */
+/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
//= require lib/utils/bootstrap_linked_tabs
((global) => {
-
class Pipelines {
constructor(options = {}) {
-
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
}
@@ -37,5 +35,4 @@
}
global.Pipelines = Pipelines;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 89f7e976934..07eea98e737 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -7,6 +7,7 @@
//
(function () {
var lastTextareaPreviewed;
+ var lastTextareaHeight = null;
var markdownPreview;
var previewButtonSelector;
var writeButtonSelector;
@@ -104,10 +105,14 @@
if (!$form) {
return;
}
+
lastTextareaPreviewed = $form.find('textarea.markdown-area');
+ lastTextareaHeight = lastTextareaPreviewed.height();
+
// toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active');
+
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
@@ -119,9 +124,15 @@
return;
}
lastTextareaPreviewed = null;
+
+ if (lastTextareaHeight) {
+ $form.find('textarea.markdown-area').height(lastTextareaHeight);
+ }
+
// toggle tabs
$form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active');
+
// toggle content
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
index b4b6da41f63..42e9847af91 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,14 +1,12 @@
-/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */
+/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */
((global) => {
-
// Matches everything but the file name
const FILENAMEREGEX = /^.*[\\\/]/;
class GitLabCrop {
constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
-
this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
this.onModalHide = this.onModalHide.bind(this);
this.onModalShow = this.onModalShow.bind(this);
@@ -136,7 +134,7 @@
var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
- for (k = i = 0, len = binary.length; i < len; k = ++i) {
+ for (k = i = 0, len = binary.length; i < len; k = (i += 1)) {
v = binary[k];
array.push(binary.charCodeAt(k));
}
@@ -169,6 +167,5 @@
return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts));
});
- }
-
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index aef2e3a3fa8..6dbaae25f2a 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -1,8 +1,7 @@
-/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */
+/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
/* global Flash */
((global) => {
-
class Profile {
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
@@ -95,5 +94,4 @@
return new Profile();
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index fcf3a4af956..7cf630a1d76 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global Cookies */
/* global Turbolinks */
/* global ProjectSelect */
@@ -94,11 +94,11 @@
return $el.text().trim();
},
clicked: function(selected, $el, e) {
- e.preventDefault()
+ e.preventDefault();
if ($('input[name="ref"]').length) {
- var $form = $dropdown.closest('form'),
- action = $form.attr('action'),
- divider = action.indexOf('?') < 0 ? '?' : '&';
+ var $form = $dropdown.closest('form');
+ var action = $form.attr('action');
+ var divider = action.indexOf('?') < 0 ? '?' : '&';
Turbolinks.visit(action + '' + divider + '' + $form.serialize());
}
}
@@ -107,7 +107,5 @@
};
return Project;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index 84f28ede4bf..a6d3ba9eb86 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
@@ -16,7 +16,5 @@
}
return ProjectAvatar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 1bd232314d0..04fe84683f3 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */
/* global fuzzaldrinPlus */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.ProjectFindFile = (function() {
var highlighter;
@@ -71,7 +71,7 @@
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
- for (i = j = 0, len = filePaths.length; j < len; i = ++j) {
+ for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) {
filePath = filePaths[i];
if (i === 20) {
break;
@@ -92,7 +92,7 @@
lastIndex = 0;
highlightText = "";
matchedChars = [];
- for (j = 0, len = matches.length; j < len; j++) {
+ for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
if (unmatched) {
@@ -167,7 +167,5 @@
};
return ProjectFindFile;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index 4aedc9a2330..208f25a0e33 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
(function() {
this.ProjectFork = (function() {
function ProjectFork() {
@@ -9,7 +9,5 @@
}
return ProjectFork;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 02dafcfb865..6614d8952cd 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
/* global Turbolinks */
(function() {
@@ -10,7 +10,5 @@
}
return ProjectImport;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
index b8d6a198996..8365f7118d5 100644
--- a/app/assets/javascripts/project_label_subscription.js.es6
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */
(function(global) {
class ProjectLabelSubscription {
@@ -38,8 +38,8 @@
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
- for (let button of this.$buttons) {
- let $button = $(button);
+ for (const button of this.$buttons) {
+ const $button = $(button);
if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
@@ -50,5 +50,4 @@
}
global.ProjectLabelSubscription = ProjectLabelSubscription;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 7fc611d0dad..3aa6f6771ce 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, one-var, indent, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, radix, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.ProjectNew = (function() {
function ProjectNew() {
@@ -14,18 +15,29 @@
return $('.save-project-loader').show();
};
})(this));
+
+ this.initVisibilitySelect();
+
this.toggleSettings();
this.toggleSettingsOnclick();
this.toggleRepoVisibility();
}
+ ProjectNew.prototype.initVisibilitySelect = function() {
+ const visibilityContainer = document.querySelector('.js-visibility-select');
+ if (!visibilityContainer) return;
+ const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
+ visibilitySelect.init();
+ };
+
ProjectNew.prototype.toggleSettings = function() {
var self = this;
this.$selects.each(function () {
- var $select = $(this),
- className = $select.data('field').replace(/_/g, '-')
- .replace('access-level', 'feature');
+ var $select = $(this);
+ var className = $select.data('field')
+ .replace(/_/g, '-')
+ .replace('access-level', 'feature');
self._showOrHide($select, '.' + className);
});
};
@@ -45,9 +57,9 @@
};
ProjectNew.prototype.toggleRepoVisibility = function () {
- var $repoAccessLevel = $('.js-repo-access-level select'),
- containerRegistry = document.querySelectorAll('.js-container-registry')[0],
- containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
+ var $repoAccessLevel = $('.js-repo-access-level select');
+ var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
+ var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
@@ -55,11 +67,11 @@
$repoAccessLevel.off('change')
.on('change', function () {
- var selectedVal = parseInt($repoAccessLevel.val());
+ var selectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.each(function () {
- var $this = $(this),
- repoSelectVal = parseInt($this.val());
+ var $this = $(this);
+ var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show();
@@ -88,7 +100,5 @@
};
return ProjectNew;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 38bc2e1c3a0..7b5e9953598 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
/* global Api */
(function() {
@@ -100,7 +100,5 @@
}
return ProjectSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index eaf4c03d573..aad130cf267 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -1,12 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
+
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
return ProjectShow;
-
})();
-
}).call(this);
// I kept class for future
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 4548dc68fe1..69a11dfaf39 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */
(function() {
window.ProjectsList = {
@@ -47,5 +47,4 @@
});
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 4aef1c84b56..e7fff57ff45 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,7 +1,7 @@
-/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchAccessDropdown = class {
constructor(options) {
@@ -25,6 +25,5 @@
}
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index f26fba979a4..57ea2f52814 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,8 +1,8 @@
-/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global ProtectedBranchDropdown */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchCreate = class {
constructor() {
@@ -44,7 +44,6 @@
// This will run after clicked callback
onSelect() {
-
// Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
@@ -52,6 +51,5 @@
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index 4ff2fa5a80f..149e511451e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,8 +1,8 @@
-/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global Flash */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchEdit = class {
constructor(options) {
@@ -14,7 +14,6 @@
}
buildDropdowns() {
-
// Allowed to merge dropdown
new gl.ProtectedBranchAccessDropdown({
$dropdown: this.$allowedToMergeDropdown,
@@ -63,6 +62,5 @@
}
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index b6972ef2e16..336fa6c57a7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,10 +1,10 @@
-/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */
(global => {
- global.gl = global.gl || {};
+ global.gl = global.gl || {};
gl.ProtectedBranchEditList = class {
- constructor() {
+ constructor() {
this.$wrap = $('.protected-branches-list');
// Build edit forms
@@ -14,6 +14,5 @@
});
});
}
- }
-
+ };
})(window);
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index bbb2f186655..0caf8ba4344 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len */
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math
@@ -12,5 +12,4 @@
$(document).on('ready page:load', function() {
return $('body').renderGFM();
});
-
}).call(this);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
index 209e7a8661d..6cef449babf 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/render_math.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len, no-console */
// Renders math using KaTeX in any element with the
// `js-render-math` class
//
@@ -51,5 +51,4 @@
});
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index b1e844b7302..76a0f993ea0 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,8 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
/* global Cookies */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Sidebar = (function() {
function Sidebar(currentUser) {
@@ -18,7 +18,7 @@
$('.dropdown').off('loading.gl.dropdown');
$('.dropdown').off('loaded.gl.dropdown');
$(document).off('click', '.js-sidebar-toggle');
- }
+ };
Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
@@ -200,7 +200,5 @@
};
return Sidebar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 5945cab4cf0..489e567259c 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
/* global Api */
(function() {
@@ -96,7 +96,5 @@
};
return Search;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index cec8856d4e7..480755899fb 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,7 +1,6 @@
-/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */
+/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
((global) => {
-
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
@@ -105,7 +104,7 @@
data = [];
// List results
firstCategory = true;
- for (i = 0, len = response.length; i < len; i++) {
+ for (i = 0, len = response.length; i < len; i += 1) {
suggestion = response[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
@@ -216,7 +215,7 @@
this.dropdown.addClass('open').trigger('shown.bs.dropdown');
return this.searchInput.removeClass('disabled');
}
- };
+ }
// Saves last length of the entered text
onSearchInputKeyDown() {
@@ -280,12 +279,12 @@
return this.searchInput.val();
}
- onClearInputClick(e) {
+ onClearInputClick(e) {
e.preventDefault();
return this.searchInput.val('').focus();
}
- onSearchInputBlur(e) {
+ onSearchInputBlur(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
@@ -305,12 +304,12 @@
hasLocationBadge() {
return this.wrap.is('.has-location-badge');
- };
+ }
restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
- for (i = 0, len = inputs.length; i < len; i++) {
+ for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
this.getElement("#" + input).val(this.originalState[input]);
}
@@ -331,7 +330,7 @@
var i, input, inputs, len, results;
inputs = Object.keys(this.originalState);
results = [];
- for (i = 0, len = inputs.length; i < len; i++) {
+ for (i = 0, len = inputs.length; i < len; i += 1) {
input = inputs[i];
// _location isnt a input
if (input === '_location') {
@@ -361,7 +360,7 @@
var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html);
- };
+ }
onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) {
@@ -384,8 +383,7 @@
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
- };
-
+ }
}
global.SearchAutocomplete = SearchAutocomplete;
@@ -426,5 +424,4 @@
};
}
});
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 5ea00f408f4..c56ee429b8e 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,10 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
/* global Turbolinks */
/* global findFileURL */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Shortcuts = (function() {
function Shortcuts(skipResetBindings) {
@@ -51,7 +51,7 @@
var i, l, len, results;
if (location && location.length > 0) {
results = [];
- for (i = 0, len = location.length; i < len; i++) {
+ for (i = 0, len = location.length; i < len; i += 1) {
l = location[i];
results.push($(l).show());
}
@@ -78,7 +78,6 @@
};
return Shortcuts;
-
})();
$(document).on('click.more_help', '.js-more-help-button', function(e) {
@@ -99,5 +98,4 @@
}
};
})();
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index c26903038b4..d50ddd98de1 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */
/* global Shortcuts */
/* global Mousetrap */
@@ -25,7 +25,5 @@
};
return ShortcutsBlob;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 4549742bbcb..603fefbf15a 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */
/* global Shortcuts */
@@ -36,7 +36,5 @@
};
return ShortcutsDashboardNavigation;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 3a81380eef0..8469837533b 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */
/* global Mousetrap */
/* global ShortcutsNavigation */
@@ -34,7 +34,5 @@
}
return ShortcutsFindFile;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index b892fbc3393..6f919de3da7 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
/* global Turbolinks */
/* global ShortcutsNavigation */
@@ -76,7 +76,5 @@
};
return ShortcutsIssuable;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 0776d0a9b67..afeda0dd5fe 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */
/* global Shortcuts */
@@ -64,7 +64,5 @@
};
return ShortcutsNavigation;
-
})(Shortcuts);
-
}).call(this);
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index ecc3fab84c3..79896e35cbb 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */
/* global Mousetrap */
/* global ShortcutsNavigation */
@@ -24,7 +24,5 @@
}
return ShortcutsNetwork;
-
})(ShortcutsNavigation);
-
}).call(this);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index 9790a44972d..05234643c18 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */
+/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
((global) => {
@@ -94,5 +94,4 @@
}
global.Sidebar = Sidebar;
-
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 9602526063e..5b20c63384c 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,7 +1,7 @@
-/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
@@ -86,7 +86,6 @@
};
return SingleFileDiff;
-
})();
$.fn.singleFileDiff = function() {
@@ -96,5 +95,4 @@
}
});
};
-
}).call(this);
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 18512d179b3..cfb4ff82a73 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,15 +1,14 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
/*= require_tree . */
(function() {
$(function() {
- var editor = ace.edit("editor")
+ var editor = ace.edit("editor");
$(".snippet-form-holder form").on('submit', function() {
$(".snippet-file-content").val(editor.getValue());
});
});
-
}).call(this);
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
index 6f913326a3a..2128007113f 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */
+/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */
(global => {
global.gl = global.gl || {};
@@ -9,5 +9,5 @@
$holder.find('.pagination').on('ajax:success', (e, data) => {
$holder.replaceWith(data.html);
});
- }
+ };
})(window);
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index f1fc526bf2e..531fd0e9c32 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */
(function() {
@@ -26,7 +26,5 @@
}
return Star;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6
index 932120157a3..d8191605128 100644
--- a/app/assets/javascripts/subbable_resource.js.es6
+++ b/app/assets/javascripts/subbable_resource.js.es6
@@ -1,6 +1,3 @@
-//= require vue
-//= require vue-resource
-
(() => {
/*
* SubbableResource can be extended to provide a pubsub-style service for one-off REST
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 185d20775d0..187356f0bf9 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.SubscriptionSelect = (function() {
function SubscriptionSelect() {
@@ -30,7 +30,5 @@
}
return SubscriptionSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 5d0fa62c50a..115716bff6a 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */
// Syntax Highlighter
//
@@ -10,7 +10,6 @@
// <div class="js-syntax-highlight"></div>
//
(function() {
-
$.fn.syntaxHighlight = function() {
var $children;
@@ -25,5 +24,4 @@
}
}
};
-
}).call(this);
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index bdec948fb63..b0132af70f2 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */
+/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
/*= require ../blob/template_selector */
@@ -12,7 +12,7 @@
this.issuableType = this.wrapper.data('issuable-type');
this.titleInput = $(`#${this.issuableType}_title`);
- let initialQuery = {
+ const initialQuery = {
name: this.dropdown.data('selected')
};
@@ -47,10 +47,10 @@
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: true });
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
+ this.requestFileSuccess(this.currentTemplate, { skipFocus: false });
}
return;
}
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index 7310b9de074..97f6d37364d 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */
+/* eslint-disable no-new, comma-dangle, class-methods-use-this, no-param-reassign */
((global) => {
class IssuableTemplateSelectors {
@@ -19,7 +19,7 @@
}
initEditor() {
- let editor = $('.markdown-area');
+ const editor = $('.markdown-area');
// Proxy ace-editor's .setValue to jQuery's .val
editor.setValue = editor.val;
editor.getValue = editor.val;
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index d8713600030..ef9c0a885fb 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,9 +1,8 @@
-/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */
+/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */
/* global UsersSelect */
/* global Turbolinks */
((global) => {
-
class Todos {
constructor({ el } = {}) {
this.allDoneClicked = this.allDoneClicked.bind(this);
@@ -49,7 +48,7 @@
clicked: function() {
return $dropdown.closest('form.filter-form').submit();
}
- })
+ });
}
doneClicked(e) {
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index f48a7ee0f55..d124ca4f88b 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
/* global Turbolinks */
(function() {
this.TreeView = (function() {
@@ -64,7 +64,5 @@
};
return TreeView;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/authenticate.js.es6 b/app/assets/javascripts/u2f/authenticate.js.es6
index 2b992109a8c..500b78fc5d8 100644
--- a/app/assets/javascripts/u2f/authenticate.js.es6
+++ b/app/assets/javascripts/u2f/authenticate.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
/* global u2f */
/* global U2FError */
/* global U2FUtil */
@@ -10,7 +10,7 @@
(function() {
const global = window.gl || (window.gl = {});
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
global.U2FAuthenticate = (function() {
function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) {
@@ -57,7 +57,7 @@
return function(response) {
var error;
if (response.errorCode) {
- error = new U2FError(response.errorCode);
+ error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error);
} else {
return _this.renderAuthenticated(JSON.stringify(response));
@@ -114,7 +114,5 @@
};
return U2FAuthenticate;
-
})();
-
})();
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index bb9942a3aa0..86b459e1866 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,29 +1,27 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */
/* global u2f */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FError = (function() {
- function U2FError(errorCode) {
+ function U2FError(errorCode, u2fFlowType) {
this.errorCode = errorCode;
this.message = bind(this.message, this);
this.httpsDisabled = window.location.protocol !== 'https:';
+ this.u2fFlowType = u2fFlowType;
}
U2FError.prototype.message = function() {
- switch (false) {
- case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled):
- return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details.";
- case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE:
- return "This device has already been registered with us.";
- default:
- return "There was a problem communicating with your device.";
+ if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
+ return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) {
+ if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.';
+ if (this.u2fFlowType === 'register') return 'This device has already been registered with us.';
}
+ return "There was a problem communicating with your device.";
};
return U2FError;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 050c9bfc02e..69d1ff3a39e 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */
/* global u2f */
/* global U2FError */
/* global U2FUtil */
@@ -8,7 +8,7 @@
// State Flow #1: setup -> in_progress -> registered -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.U2FRegister = (function() {
function U2FRegister(container, u2fParams) {
@@ -39,7 +39,7 @@
return function(response) {
var error;
if (response.errorCode) {
- error = new U2FError(response.errorCode);
+ error = new U2FError(response.errorCode, 'register');
return _this.renderError(error);
} else {
return _this.renderRegistered(JSON.stringify(response));
@@ -94,7 +94,5 @@
};
return U2FRegister;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index eedd3bcd5a1..34e88220b12 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife */
(function() {
this.U2FUtil = (function() {
function U2FUtil() {}
@@ -8,7 +8,5 @@
};
return U2FUtil;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 0a2db7c05fe..059e6c628b3 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */
+/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */
/* global Cookies */
((global) => {
@@ -30,5 +30,5 @@
$(this).parents('.project-limit-message').remove();
});
}
- }
+ };
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index b9c23b51b4d..313fb17aee8 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign */
/*
UserTabs
@@ -107,7 +107,7 @@ content on the Users#show page.
this.loadActivities(source);
}
- const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
+ const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action);
}
@@ -145,7 +145,7 @@ content on the Users#show page.
}
setCurrentAction(source, action) {
- let new_state = source
+ let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 578be7c3590..7ffc546ffc1 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,9 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */
/* global d3 */
/* global dateFormat */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Calendar = (function() {
function Calendar(timestamps, calendar_activities_path) {
@@ -20,7 +20,7 @@
this.timestampsTmp = [];
var group = 0;
- var today = new Date()
+ var today = new Date();
today.setHours(0, 0, 0, 0, 0);
var oneYearAgo = new Date(today);
@@ -28,7 +28,7 @@
var days = gl.utils.getDayDifference(oneYearAgo, today);
- for(var i = 0; i <= days; i++) {
+ for (var i = 0; i <= days; i += 1) {
var date = new Date(oneYearAgo);
date.setDate(date.getDate() + i);
@@ -39,7 +39,7 @@
// or if is first object
if ((day === 0 && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
- group++;
+ group += 1;
}
var innerArray = this.timestampsTmp[group - 1];
@@ -74,7 +74,7 @@
}
return extraWidthPadding;
- }
+ };
Calendar.prototype.renderSvg = function(group) {
var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
@@ -221,7 +221,5 @@
};
return Calendar;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index d4b5e03aa35..77d2764cdf0 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,10 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
/* global Vue */
/* global Issuable */
/* global ListUser */
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice;
this.UsersSelect = (function() {
@@ -116,7 +116,7 @@
showDivider = 0;
if (firstUser) {
// Move current user to the front of the list
- for (index = j = 0, len = users.length; j < len; index = ++j) {
+ for (index = j = 0, len = users.length; j < len; index = (j += 1)) {
obj = users[index];
if (obj.username === firstUser) {
users.splice(index, 1);
@@ -278,7 +278,7 @@
if (firstUser) {
// Move current user to the front of the list
ref = data.results;
- for (index = j = 0, len = ref.length; j < len; index = ++j) {
+ for (index = j = 0, len = ref.length; j < len; index = (j += 1)) {
obj = ref[index];
if (obj.username === firstUser) {
data.results.splice(index, 1);
@@ -371,7 +371,7 @@
};
UsersSelect.prototype.user = function(user_id, callback) {
- if(!/^\d+$/.test(user_id)) {
+ if (!/^\d+$/.test(user_id)) {
return false;
}
@@ -421,7 +421,5 @@
};
return UsersSelect;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6
new file mode 100644
index 00000000000..1fa2b5ac399
--- /dev/null
+++ b/app/assets/javascripts/version_check_image.js.es6
@@ -0,0 +1,10 @@
+(() => {
+ class VersionCheckImage {
+ static bindErrorEvent(imageElement) {
+ imageElement.off('error').on('error', () => imageElement.hide());
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.VersionCheckImage = VersionCheckImage;
+})();
diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js.es6
new file mode 100644
index 00000000000..f712d7ba930
--- /dev/null
+++ b/app/assets/javascripts/visibility_select.js.es6
@@ -0,0 +1,27 @@
+(() => {
+ const gl = window.gl || (window.gl = {});
+
+ class VisibilitySelect {
+ constructor(container) {
+ if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
+ this.container = container;
+ this.helpBlock = this.container.querySelector('.help-block');
+ this.select = this.container.querySelector('select');
+ }
+
+ init() {
+ if (this.select) {
+ this.updateHelpText();
+ this.select.addEventListener('change', this.updateHelpText.bind(this));
+ } else {
+ this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
+ }
+ }
+
+ updateHelpText() {
+ this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
+ }
+ }
+
+ gl.VisibilitySelect = VisibilitySelect;
+})();
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
index ad5cb30cc42..b195b0ef3ba 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -22,47 +22,51 @@
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
- <a
+ <button
v-if='actions'
- class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
- alt="Manual Build"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Manual build"
>
- <span v-html='svgs.iconPlay'></span>
- <i class="fa fa-caret-down"></i>
- </a>
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
- title="Manual build"
>
- <span v-html='svgs.iconPlay'></span>
- <span title="Manual build">{{action.name}}</span>
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
- <a
+ <button
v-if='artifacts'
- class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ data-toggle="dropdown"
+ title="Artifacts"
+ data-placement="top"
data-toggle="dropdown"
- type="button"
+ aria-label="Artifacts"
>
- <i class="fa fa-download"></i>
- <i class="fa fa-caret-down"></i>
- </a>
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
- <i class="fa fa-download"></i>
+ <i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
@@ -76,9 +80,12 @@
title="Retry"
rel="nofollow"
data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
:href='pipeline.retry_path'
+ aria-label="Retry"
>
- <i class="fa fa-repeat"></i>
+ <i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
@@ -86,10 +93,12 @@
title="Cancel"
rel="nofollow"
data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
:href='pipeline.cancel_path'
- data-original-title="Cancel"
+ aria-label="Cancel"
>
- <i class="fa fa-remove"></i>
+ <i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</div>
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
index f075a995846..496df9aaced 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -1,28 +1,54 @@
/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign, no-bitwise */
+/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStage = Vue.extend({
data() {
return {
- count: 0,
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
- props: ['stage', 'svgs', 'match'],
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ svgs: {
+ type: DOMStringMap,
+ required: true,
+ },
+ match: {
+ type: Function,
+ required: true,
+ },
+ },
methods: {
- fetchBuilds() {
- if (this.count > 0) return null;
+ fetchBuilds(e) {
+ const areaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
+
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
- this.count += 1;
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
+ keepGraph(e) {
+ const { target } = e;
+
+ if (target.className.indexOf('js-ci-action-icon') >= 0) return null;
+
+ if (
+ target.parentElement &&
+ (target.parentElement.className.indexOf('js-ci-action-icon') >= 0)
+ ) return null;
+
+ return e.stopPropagation();
+ },
},
computed: {
buildsOrSpinner() {
@@ -39,7 +65,7 @@
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
svg() {
- const icon = this.stage.status.icon;
+ const { icon } = this.stage.status;
const stageIcon = icon.replace(/icon/i, 'stage_icon');
return this.svgs[this.match(stageIcon)];
},
@@ -50,18 +76,26 @@
template: `
<div>
<button
- @click='fetchBuilds'
+ @click='fetchBuilds($event)'
:class="triggerButtonClass"
:title='stage.title'
data-placement="top"
data-toggle="dropdown"
- type="button">
- <span v-html="svg"></span>
- <i class="fa fa-caret-down "></i>
+ type="button"
+ :aria-label='stage.title'
+ >
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up"></div>
- <div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div>
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ @click='keepGraph($event)'
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner"
+ >
+ </div>
</ul>
</div>
`,
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
index 1982142853a..9e19b1564dc 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -4,19 +4,15 @@
((gl) => {
const pageValues = (headers) => {
- const normalizedHeaders = {};
-
- Object.keys(headers).forEach((e) => {
- normalizedHeaders[e.toUpperCase()] = headers[e];
- });
+ const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = {
- perPage: +normalizedHeaders['X-PER-PAGE'],
- page: +normalizedHeaders['X-PAGE'],
- total: +normalizedHeaders['X-TOTAL'],
- totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
- nextPage: +normalizedHeaders['X-NEXT-PAGE'],
- previousPage: +normalizedHeaders['X-PREV-PAGE'],
+ perPage: +normalized['X-PER-PAGE'],
+ page: +normalized['X-PAGE'],
+ total: +normalized['X-TOTAL'],
+ totalPages: +normalized['X-TOTAL-PAGES'],
+ nextPage: +normalized['X-NEXT-PAGE'],
+ previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo;
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index e09b59dd5aa..a8b7be7ad06 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */
/* global Dropzone */
/* global Mousetrap */
@@ -93,7 +93,5 @@
};
return ZenMode;
-
})();
-
}).call(this);
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 407c800feb7..592ef0d647f 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -82,7 +82,12 @@
}
.block-controls {
- float: right;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+ -webkit-flex: 1;
+ flex: 1;
.control {
float: left;
@@ -282,3 +287,8 @@
}
}
}
+
+.flex-container-block {
+ display: -webkit-flex;
+ display: flex;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index e04a87a7327..bb6129158d9 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -324,7 +324,7 @@
&:focus {
cursor: text;
box-shadow: none;
- border-color: $border-color;
+ border-color: lighten($dropdown-input-focus-border, 20%);
color: $gray-darkest;
background-color: $gray-light;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index fee38b05023..4b05ec691a8 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -76,7 +76,17 @@
.filter-dropdown {
max-height: 215px;
- overflow-x: scroll;
+ overflow: auto;
+}
+
+%filter-dropdown-item-btn-hover {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ text-decoration: none;
+
+ .avatar {
+ border-color: $white-light;
+ }
}
.filter-dropdown-item {
@@ -86,7 +96,7 @@
text-align: left;
padding: 8px 16px;
text-overflow: ellipsis;
- overflow-y: hidden;
+ overflow: hidden;
border-radius: 0;
.fa {
@@ -103,13 +113,7 @@
&:hover,
&:focus {
- background-color: $dropdown-hover-color;
- color: $white-light;
- text-decoration: none;
-
- .avatar {
- border-color: $white-light;
- }
+ @extend %filter-dropdown-item-btn-hover;
}
}
@@ -131,6 +135,12 @@
}
}
+.filter-dropdown-item.dropdown-active {
+ .btn {
+ @extend %filter-dropdown-item-btn-hover;
+ }
+}
+
.hint-dropdown {
width: 250px;
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index dccf5177e35..868f28cd356 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -15,6 +15,7 @@
}
.ci-status-icon-pending,
+.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
color: $gl-warning;
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
index 4decee2c525..5f4211147f3 100644
--- a/app/assets/stylesheets/framework/page-header.scss
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -46,10 +46,6 @@
font-weight: bold;
}
- .fa-clipboard {
- color: $dropdown-title-btn-color;
- }
-
.commit-info {
&.branches {
margin-left: 8px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index bd58a26f429..54958973f15 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -10,7 +10,7 @@
max-width: 100%;
}
- *:first-child {
+ *:first-child:not(.katex-display) {
margin-top: 0;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 349cd9c189e..07cb669a46e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -5,7 +5,7 @@ $sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
-$gutter_inner_width: 258px;
+$gutter_inner_width: 250px;
$sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
@@ -56,6 +56,7 @@ $black-transparent: rgba(0, 0, 0, 0.3);
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
+$border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
@@ -85,6 +86,7 @@ $warning-message-border: #f0e2bb;
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
+$sidebar-collapsed-icon-color: #999;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
@@ -280,6 +282,7 @@ $dropdown-hover-color: #3b86ff;
*/
$btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed;
+$btn-white-active: #848484;
/*
* Badges
@@ -433,6 +436,7 @@ $help-shortcut-header-color: #333;
*/
$issues-today-bg: #f3fff2;
$issues-today-border: #e1e8d5;
+$compare-display-color: #888;
/*
* jQuery UI
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 0ae5dc5c537..93cc5a8cf0a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -377,6 +377,10 @@
display: inline-block;
padding: 5px;
+ &:nth-of-type(7n) {
+ padding-right: 0;
+ }
+
.author_link {
display: block;
}
@@ -473,3 +477,102 @@
}
}
}
+
+.time_tracker {
+ padding-bottom: 0;
+ border-bottom: 0;
+
+
+ .sidebar-collapsed-icon {
+
+ > .stopwatch-svg {
+ display: inline-block;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $sidebar-collapsed-icon-color;
+ }
+
+ &:hover svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ .help-button,
+ .close-help-button {
+ cursor: pointer;
+ }
+
+ .compare-meter {
+ &.within_estimate {
+ .meter-fill {
+ background: $gl-primary;
+ }
+ }
+
+ &.over_estimate {
+ .meter-fill {
+ background: $red-light;
+ }
+
+ .time-remaining,
+ .compare-value.spent {
+ color: $red-light;
+ }
+ }
+ }
+
+ .meter-container {
+ background: $border-gray-light;
+ border-radius: 3px;
+
+ .meter-fill {
+ max-width: 100%;
+ height: 5px;
+ border-radius: 3px;
+ background: $gl-primary;
+ }
+ }
+
+ .compare-display-container {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 5px;
+
+ .compare-display {
+ font-size: 13px;
+ color: $compare-display-color;
+
+ .compare-value {
+ color: $gl-text-color;
+ }
+ }
+ }
+
+ .time-tracking-help-state {
+ background: $white-light;
+ margin: 16px -20px 0;
+ padding: 16px 20px;
+ border-top: 1px solid $border-gray-light;
+ border-bottom: 1px solid $border-gray-light;
+
+ a:hover {
+ color: $btn-white-active;
+ }
+ }
+
+ .help-state-toggle-enter-active {
+ transition: all .8s ease;
+ }
+
+ .help-state-toggle-leave-active {
+ transition: all .5s ease;
+ }
+
+ .help-state-toggle-enter,
+ .help-state-toggle-leave-active {
+ opacity: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e2a0253da38..da0caa30c26 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -195,10 +195,10 @@ ul.notes {
}
.note-body {
- overflow: auto;
+ overflow-x: auto;
+ overflow-y: hidden;
.note-text {
- overflow: auto;
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
@@ -515,7 +515,6 @@ ul.notes {
.line-resolve-all-container {
.btn-group {
- margin-top: -1px;
margin-left: -4px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8861315d776..5d4bd091a6b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -44,8 +44,8 @@
.pipeline-info,
.pipeline-commit,
- .pipeline-actions,
- .pipeline-stages {
+ .pipeline-stages,
+ .pipeline-actions {
width: 20%;
}
}
@@ -185,6 +185,7 @@
.stage-cell {
font-size: 0;
+ padding: 10px 4px;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
@@ -202,8 +203,8 @@
position: relative;
margin-right: 6px;
- .tooltip {
- white-space: nowrap;
+ .tooltip-inner {
+ padding: 3px 4px;
}
&:not(:last-child) {
@@ -287,6 +288,10 @@
}
}
}
+
+ .tooltip {
+ white-space: nowrap;
+ }
}
.build-link {
@@ -348,6 +353,7 @@
padding: $gl-padding;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
+ overflow: auto;
.stage-column-list,
.builds-container > ul {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 9455ba3b98a..1b0bf4554e6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -9,17 +9,17 @@
.new_project,
.edit-project {
- fieldset {
-
- &.features {
+ .sharing-and-permissions {
+ .header {
+ padding-top: $gl-vert-padding;
+ }
- .label-light {
- margin-bottom: 0;
- }
+ .label-light {
+ margin-bottom: 0;
+ }
- .help-block {
- margin-top: 0;
- }
+ .help-block {
+ margin-top: 0;
}
.form-group {
@@ -198,7 +198,7 @@
margin: 15px 5px 0 0;
input {
- height: 28px;
+ height: 27px;
}
}
@@ -905,10 +905,18 @@ pre.light-well {
}
}
-.project-feature-nested {
+.project-feature {
+ padding-top: 10px;
+
@media (min-width: $screen-sm-min) {
padding-left: 45px;
}
+
+ &.nested {
+ @media (min-width: $screen-sm-min) {
+ padding-left: 90px;
+ }
+ }
}
.project-repo-select {
@@ -921,8 +929,32 @@ pre.light-well {
.variables-table {
table-layout: fixed;
+ &.table-responsive {
+ border: none;
+ }
+
.variable-key {
- width: 30%;
+ width: 300px;
+ max-width: 300px;
+ overflow: hidden;
+ word-wrap: break-word;
+
+ // override bootstrap
+ white-space: normal!important;
+
+ @media (max-width: $screen-sm-max) {
+ width: 150px;
+ max-width: 150px;
+ }
+ }
+
+ .variable-value {
+ @media(max-width: $screen-xs-max) {
+ width: 150px;
+ max-width: 150px;
+ overflow: hidden;
+ word-wrap: break-word;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 12bff32bbf3..88ea92c5afb 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -18,7 +18,6 @@
.file-finder-input:hover,
.issuable-search-form:hover,
.search-text-input:hover,
-textarea:hover,
.form-control:hover {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index f19275770be..6f31d4ed789 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -19,7 +19,8 @@
overflow: visible;
}
- &.ci-failed {
+ &.ci-failed,
+ &.ci-failed_with_warnings {
color: $gl-danger;
border-color: $gl-danger;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 5f13353baa1..6db4e1dc1bc 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -18,7 +18,7 @@ class AutocompleteController < ApplicationController
if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user].present? && current_user
- @users = [*@users, current_user]
+ @users = [current_user, *@users]
end
if params[:author_id].present?
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index 2aaf8f2b451..52e06f4945a 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -1,6 +1,10 @@
module CycleAnalyticsParams
extend ActiveSupport::Concern
+ def options(params)
+ @options ||= { from: start_date(params), current_user: current_user }
+ end
+
def start_date(params)
params[:start_date] == '30' ? 30.days.ago : 90.days.ago
end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 3da44b9b888..306afb65f10 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController
if signed_in?(resource_name)
after_sign_in_path_for(resource)
else
- sign_in(resource)
- if signed_in?(resource_name)
- after_sign_in_path_for(resource)
- else
- new_session_path(resource_name)
- end
+ flash[:notice] += " Please sign in."
+ new_session_path(resource_name)
end
end
end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 7051652d109..7f506db583f 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -19,11 +19,11 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
private
def milestones
- @milestones = GlobalMilestone.build_collection(@projects, params)
+ @milestones = DashboardMilestone.build_collection(@projects, params)
end
def milestone
- @milestone = GlobalMilestone.build(@projects, params[:title])
+ @milestone = DashboardMilestone.build(@projects, params[:title])
render_404 unless @milestone
end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index fbe391fc58c..9b45ed6b6af 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -94,7 +94,7 @@ class Projects::BuildsController < Projects::ApplicationController
private
def build
- @build ||= project.builds.find_by!(id: params[:id])
+ @build ||= project.builds.find_by!(id: params[:id]).present(user: current_user)
end
def build_path(build)
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 791ed88db30..bfc59bcc862 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -12,7 +12,6 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :commit
before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
- before_action :define_status_vars, only: [:show, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
@@ -106,10 +105,6 @@ class Projects::CommitController < Projects::ApplicationController
}
end
- def define_status_vars
- @ci_pipelines = project.pipelines.where(sha: commit.sha)
- end
-
def assign_change_commit_vars(mr_source_branch)
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index ec02fc15d35..d32966645c8 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController
end
def create
- redirect_to namespace_project_compare_path(@project.namespace, @project,
+ if params[:from].blank? || params[:to].blank?
+ flash[:alert] = "You must select from and to branches"
+ from_to_vars = {
+ from: params[:from].presence,
+ to: params[:to].presence
+ }
+ redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars)
+ else
+ redirect_to namespace_project_compare_path(@project.namespace, @project,
params[:from], params[:to])
+ end
end
private
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index 13b3eec761f..b69d46f2c41 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -9,56 +9,52 @@ module Projects
before_action :authorize_read_merge_request!, only: [:code, :review]
def issue
- render_events(events.issue_events)
+ render_events(cycle_analytics[:issue].events)
end
def plan
- render_events(events.plan_events)
+ render_events(cycle_analytics[:plan].events)
end
def code
- render_events(events.code_events)
+ render_events(cycle_analytics[:code].events)
end
def test
- options[:branch] = events_params[:branch_name]
+ options(events_params)[:branch] = events_params[:branch_name]
- render_events(events.test_events)
+ render_events(cycle_analytics[:test].events)
end
def review
- render_events(events.review_events)
+ render_events(cycle_analytics[:review].events)
end
def staging
- render_events(events.staging_events)
+ render_events(cycle_analytics[:staging].events)
end
def production
- render_events(events.production_events)
+ render_events(cycle_analytics[:production].events)
end
private
-
- def render_events(events_list)
+
+ def render_events(events)
respond_to do |format|
format.html
- format.json { render json: { events: events_list } }
+ format.json { render json: { events: events } }
end
end
- def events
- @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options)
- end
-
- def options
- @options ||= { from: start_date(events_params), current_user: current_user }
+ def cycle_analytics
+ @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
end
def events_params
return {} unless params[:events].present?
- params[:events].slice(:start_date, :branch_name)
+ params[:events].permit(:start_date, :branch_name)
end
end
end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index ac639ef015b..88ac3ad046b 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action :authorize_read_cycle_analytics!
def show
- @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params))
+ @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params))
- stats_values, cycle_analytics_json = generate_cycle_analytics_data
-
- @cycle_analytics_no_data = stats_values.blank?
+ @cycle_analytics_no_data = @cycle_analytics.no_stats?
respond_to do |format|
format.html
@@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def cycle_analytics_params
return {} unless params[:cycle_analytics].present?
- { start_date: params[:cycle_analytics][:start_date] }
+ params[:cycle_analytics].permit(:start_date)
end
- def generate_cycle_analytics_data
- stats_values = []
-
- cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
- [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
- [:code, "Code", "Related Merge Requests", "Time spent coding"],
- [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
- [:review, "Review", "Relative Merged Requests", "The time taken to review the code"],
- [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"],
- [:production, "Production", "Related Issues", "The total time taken from idea to production"]]
-
- stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)|
- value = @cycle_analytics.send(stage_method).presence
-
- stats_values << value.abs if value
-
- stats << {
- title: stage_text,
- description: stage_description,
- legend: stage_legend,
- value: value && !value.zero? ? distance_of_time_in_words(value) : nil
- }
-
- stats
- end
-
- issues = @cycle_analytics.summary.new_issues
- commits = @cycle_analytics.summary.commits
- deploys = @cycle_analytics.summary.deploys
-
- summary = [
- { title: "New Issue".pluralize(issues), value: issues },
- { title: "Commit".pluralize(commits), value: commits },
- { title: "Deploy".pluralize(deploys), value: deploys }
- ]
-
- cycle_analytics_hash = { summary: summary,
- stats: stats,
- permissions: @cycle_analytics.permissions(user: current_user)
+ def cycle_analytics_json
+ {
+ summary: @cycle_analytics.summary,
+ stats: @cycle_analytics.stats,
+ permissions: @cycle_analytics.permissions(user: current_user)
}
-
- [stats_values, cycle_analytics_hash]
end
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 0ae8ff98009..b668a9331e7 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController
layout "project_settings"
- def index
- @hooks = @project.hooks
- @hook = ProjectHook.new
- end
-
def create
@hook = @project.hooks.new(hook_params)
@hook.save
- if @hook.valid?
- redirect_to namespace_project_hooks_path(@project.namespace, @project)
- else
+ unless @hook.valid?
@hooks = @project.hooks.select(&:persisted?)
- render :index
+ flash[:alert] = @hook.errors.full_messages.join.html_safe
end
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
def test
@@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy
hook.destroy
- redirect_to namespace_project_hooks_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
private
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 2beb0df8a07..8472ceca329 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -33,6 +33,18 @@ class Projects::IssuesController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
+ @users = []
+
+ if params[:assignee_id].present?
+ assignee = User.find_by_id(params[:assignee_id])
+ @users.push(assignee) if assignee
+ end
+
+ if params[:author_id].present?
+ author = User.find_by_id(params[:author_id])
+ @users.push(author) if author
+ end
+
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 30c2a5d9982..17cb1d5be24 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
- def index
- @services = @project.find_or_initialize_services
- end
-
def edit
end
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
new file mode 100644
index 00000000000..fb2a4837735
--- /dev/null
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -0,0 +1,18 @@
+module Projects
+ module Settings
+ class IntegrationsController < Projects::ApplicationController
+ include ServiceParams
+
+ before_action :authorize_admin_project!
+ layout "project_settings"
+
+ def show
+ @hooks = @project.hooks
+ @hook = ProjectHook.new
+
+ # Services
+ @services = @project.find_or_initialize_services
+ end
+ end
+ end
+end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index b666aa01d6b..6576ebd5235 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -45,6 +45,8 @@ class SearchController < ApplicationController
end
@search_objects = @search_results.objects(@scope, params[:page])
+
+ check_single_commit_result
end
def autocomplete
@@ -59,4 +61,16 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json
end
+
+ private
+
+ def check_single_commit_result
+ if @search_results.single_commit_result?
+ only_commit = @search_results.objects('commits').first
+ query = params[:search].strip.downcase
+ found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query)
+
+ redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha
+ end
+ end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 5742fec4458..2159e4ce21a 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -208,6 +208,10 @@ module GitlabRoutingHelper
end
# Settings
+ def project_settings_integrations_path(project, *args)
+ namespace_project_settings_integrations_path(project.namespace, project, *args)
+ end
+
def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 1c213983a5b..e5bb8b93e76 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -30,6 +30,15 @@ module IssuablesHelper
end
end
+ def serialize_issuable(issuable)
+ case issuable
+ when Issue
+ IssueSerializer.new.represent(issuable).to_json
+ when MergeRequest
+ MergeRequestSerializer.new.represent(issuable).to_json
+ end
+ end
+
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index c6c63918fa5..eb98204285d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -409,4 +409,15 @@ module ProjectsHelper
def project_issues(project)
IssuesFinder.new(current_user, project_id: project.id).execute
end
+
+ def visibility_select_options(project, selected_level)
+ levels_options_array = Gitlab::VisibilityLevel.values.map do |level|
+ [
+ visibility_level_label(level),
+ { data: { description: visibility_level_description(level, project) } },
+ level
+ ]
+ end
+ options_for_select(levels_options_array, selected_level)
+ end
end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 9bab140e60a..715e5893a2c 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,23 +1,23 @@
module ServicesHelper
def service_event_description(event)
case event
- when "push"
+ when "push", "push_events"
"Event will be triggered by a push to the repository"
- when "tag_push"
+ when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
- when "note"
+ when "note", "note_events"
"Event will be triggered when someone adds a comment"
- when "issue"
+ when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
- when "confidential_issue"
+ when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed"
- when "merge_request"
+ when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
- when "build"
+ when "build", "build_events"
"Event will be triggered when a build status changes"
- when "wiki_page"
+ when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
- when "commit"
+ when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
end
end
@@ -26,4 +26,6 @@ module ServicesHelper
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
end
+
+ extend self
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 74de25acf9d..c568cca9e5e 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -11,9 +11,10 @@ module TodosHelper
case todo.action
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
- when Todo::BUILD_FAILED then 'The build failed for your'
+ when Todo::BUILD_FAILED then 'The build failed for'
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
+ when Todo::UNMERGEABLE then 'Could not merge'
end
end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index a674564c4ec..456598b4c28 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,7 +1,8 @@
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
- image_tag VersionCheck.new.url
+ image_url = VersionCheck.new.url
+ image_tag image_url, class: 'js-version-status-badge'
end
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 0d20c9092c4..46fa6fd9f6d 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -38,6 +38,14 @@ module Emails
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
+ def note_personal_snippet_email(recipient_id, note_id)
+ setup_note_mail(note_id, recipient_id)
+
+ @snippet = @note.noteable
+ @target_url = snippet_url(@note.noteable)
+ mail_answer_thread(@snippet, note_thread_options(recipient_id))
+ end
+
private
def note_target_url_options
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0bc1c19e9cd..0cd3456b4de 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -107,15 +107,11 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
add_project_headers
+ add_unsubscription_headers_and_links
+
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
- if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
- headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
-
- @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
- end
-
if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
@@ -171,4 +167,16 @@ class Notify < BaseMailer
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end
+
+ def add_unsubscription_headers_and_links
+ return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable?
+
+ list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)]
+ if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard?
+ list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}"
+ end
+
+ headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
+ @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
+ end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index fa8f8bc3a5f..ad6c588202e 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -22,6 +22,17 @@ class Ability
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)
+ case snippet.visibility_level
+ when Snippet::INTERNAL, Snippet::PUBLIC
+ users
+ when Snippet::PRIVATE
+ users.include?(snippet.author) ? [snippet.author] : []
+ end
+ end
+
# Returns an Array of Issues that can be read by the given user.
#
# issues - The issues to reduce down to those readable by the user.
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8fab77cda0a..e33a58d3771 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,6 +13,49 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters
}x
+ DEFAULTS_CE = {
+ after_sign_up_text: nil,
+ akismet_enabled: false,
+ container_registry_token_expire_delay: 5,
+ default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ default_projects_limit: Settings.gitlab['default_projects_limit'],
+ default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ disabled_oauth_sign_in_sources: [],
+ domain_whitelist: Settings.gitlab['domain_whitelist'],
+ gravatar_enabled: Settings.gravatar['enabled'],
+ help_page_text: nil,
+ housekeeping_bitmaps_enabled: true,
+ housekeeping_enabled: true,
+ housekeeping_full_repack_period: 50,
+ housekeeping_gc_period: 200,
+ housekeeping_incremental_repack_period: 10,
+ import_sources: Gitlab::ImportSources.values,
+ koding_enabled: false,
+ koding_url: nil,
+ max_artifacts_size: Settings.artifacts['max_size'],
+ max_attachment_size: Settings.gitlab['max_attachment_size'],
+ plantuml_enabled: false,
+ plantuml_url: nil,
+ recaptcha_enabled: false,
+ repository_checks_enabled: true,
+ repository_storages: ['default'],
+ require_two_factor_authentication: false,
+ restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
+ session_expire_delay: Settings.gitlab['session_expire_delay'],
+ send_user_confirmation_email: false,
+ shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
+ shared_runners_text: nil,
+ sidekiq_throttling_enabled: false,
+ sign_in_text: nil,
+ signin_enabled: Settings.gitlab['signin_enabled'],
+ signup_enabled: Settings.gitlab['signup_enabled'],
+ two_factor_grace_period: 48,
+ user_default_external: false
+ }
+
+ DEFAULTS = DEFAULTS_CE
+
serialize :restricted_visibility_levels
serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array
@@ -163,46 +206,7 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.create_from_defaults
- create(
- default_projects_limit: Settings.gitlab['default_projects_limit'],
- default_branch_protection: Settings.gitlab['default_branch_protection'],
- signup_enabled: Settings.gitlab['signup_enabled'],
- signin_enabled: Settings.gitlab['signin_enabled'],
- gravatar_enabled: Settings.gravatar['enabled'],
- sign_in_text: nil,
- after_sign_up_text: nil,
- help_page_text: nil,
- shared_runners_text: nil,
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- max_attachment_size: Settings.gitlab['max_attachment_size'],
- session_expire_delay: Settings.gitlab['session_expire_delay'],
- default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: Gitlab::ImportSources.values,
- shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
- max_artifacts_size: Settings.artifacts['max_size'],
- require_two_factor_authentication: false,
- two_factor_grace_period: 48,
- recaptcha_enabled: false,
- akismet_enabled: false,
- koding_enabled: false,
- koding_url: nil,
- plantuml_enabled: false,
- plantuml_url: nil,
- repository_checks_enabled: true,
- disabled_oauth_sign_in_sources: [],
- send_user_confirmation_email: false,
- container_registry_token_expire_delay: 5,
- repository_storages: ['default'],
- user_default_external: false,
- sidekiq_throttling_enabled: false,
- housekeeping_enabled: true,
- housekeeping_bitmaps_enabled: true,
- housekeeping_incremental_repack_period: 10,
- housekeeping_full_repack_period: 50,
- housekeeping_gc_period: 200,
- )
+ create(DEFAULTS)
end
def home_page_url_column_exist
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 48ffe40abc6..5fe8ddf69d7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -2,6 +2,7 @@ module Ci
class Build < CommitStatus
include TokenAuthenticatable
include AfterCommitQueue
+ include Presentable
belongs_to :runner
belongs_to :trigger_request
@@ -91,6 +92,12 @@ module Ci
end
state_machine :status do
+ after_transition any => [:pending] do |build|
+ build.run_after_commit do
+ BuildQueueWorker.perform_async(id)
+ end
+ end
+
after_transition pending: :running do |build|
build.run_after_commit do
BuildHooksWorker.perform_async(id)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 2a97e8bae4a..fab8497ec7d 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -128,16 +128,21 @@ module Ci
end
def stages
+ # TODO, this needs refactoring, see gitlab-ce#26481.
+
+ stages_query = statuses
+ .group('stage').select(:stage).order('max(stage_idx)')
+
status_sql = statuses.latest.where('stage=sg.stage').status_sql
- stages_query = statuses.group('stage').select(:stage)
- .order('max(stage_idx)')
+ warnings_sql = statuses.latest.select('COUNT(*) > 0')
+ .where('stage=sg.stage').failed_but_allowed.to_sql
- stages_with_statuses = CommitStatus.from(stages_query, :sg).
- pluck('sg.stage', status_sql)
+ stages_with_statuses = CommitStatus.from(stages_query, :sg)
+ .pluck('sg.stage', status_sql, "(#{warnings_sql})")
stages_with_statuses.map do |stage|
- Ci::Stage.new(self, name: stage.first, status: stage.last)
+ Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 123930273e0..ed1843ba005 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
+ RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
@@ -21,6 +22,8 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
+ after_save :tick_runner_queue, if: :form_editable_changed?
+
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.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
@@ -122,8 +125,38 @@ module Ci
]
end
+ def tick_runner_queue
+ SecureRandom.hex.tap do |new_update|
+ Gitlab::Redis.with do |redis|
+ redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME)
+ end
+ end
+ end
+
+ def ensure_runner_queue_value
+ Gitlab::Redis.with do |redis|
+ value = SecureRandom.hex
+ redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true)
+ redis.get(runner_queue_key)
+ end
+ end
+
+ def is_runner_queue_value_latest?(value)
+ ensure_runner_queue_value == value if value.present?
+ end
+
private
+ def runner_queue_key
+ "runner:build_queue:#{self.token}"
+ end
+
+ def form_editable_changed?
+ FORM_EDITABLE.any? do |editable|
+ public_send("#{editable}_changed?")
+ end
+ end
+
def tag_constraints
unless has_tags? || run_untagged?
errors.add(:tags_list,
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index d035eda6df5..ca74c91b062 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -8,10 +8,11 @@ module Ci
delegate :project, to: :pipeline
- def initialize(pipeline, name:, status: nil)
+ def initialize(pipeline, name:, status: nil, warnings: nil)
@pipeline = pipeline
@name = name
@status = status
+ @warnings = warnings
end
def to_param
@@ -39,5 +40,17 @@ module Ci
def builds
@builds ||= pipeline.builds.where(stage: name)
end
+
+ def success?
+ status.to_s == 'success'
+ end
+
+ def has_warnings?
+ if @warnings.nil?
+ statuses.latest.failed_but_allowed.any?
+ else
+ @warnings
+ end
+ end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 3365f4ffdbf..316bd2e512b 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -21,6 +21,9 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
+ # The SHA can be between 7 and 40 hex characters.
+ COMMIT_SHA_PATTERN = '\h{7,40}'
+
class << self
def decorate(commits, project)
commits.map do |commit|
@@ -52,6 +55,10 @@ class Commit
def from_hash(hash, project)
new(Gitlab::Git::Commit.new(hash), project)
end
+
+ def valid_hash?(key)
+ !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
+ end
end
attr_accessor :raw
@@ -77,8 +84,6 @@ class Commit
# Pattern used to extract commit references from text
#
- # The SHA can be between 7 and 40 hex characters.
- #
# This pattern supports cross-project references.
def self.reference_pattern
@reference_pattern ||= %r{
@@ -88,7 +93,7 @@ class Commit
end
def self.link_reference_pattern
- @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
+ @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
def to_reference(from_project = nil, full: false)
@@ -326,6 +331,12 @@ class Commit
# no-op but needs to be defined since #persisted? is defined
end
+ WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
+
+ def work_in_progress?
+ !!(title =~ WIP_REGEX)
+ end
+
private
def commit_reference(from_project, referable_commit_id, full: false)
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 90bd6490a02..a600f9c14c5 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -51,6 +51,10 @@ module CacheMarkdownField
CACHING_CLASSES.map(&:constantize)
end
+ def skip_project_check?
+ false
+ end
+
extend ActiveSupport::Concern
included do
@@ -112,7 +116,8 @@ module CacheMarkdownField
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
- html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
+ options = { skip_project_check: skip_project_check? }
+ html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 90432fc4050..431c0354969 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,6 +1,7 @@
module HasStatus
extend ActiveSupport::Concern
+ DEFAULT_STATUS = 'created'
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5e63825bf99..3517969eabc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -13,6 +13,7 @@ module Issuable
include StripAttribute
include Awardable
include Taskable
+ include TimeTrackable
included do
cache_markdown_field :title, pipeline: :single_line
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 8ab0401d288..ef2c1e5d414 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -49,7 +49,11 @@ module Mentionable
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
- options = options.merge(cache_key: [self, attr], author: author)
+ options = options.merge(
+ cache_key: [self, attr],
+ author: author,
+ skip_project_check: skip_project_check?
+ )
extractor.analyze(text, options)
end
@@ -121,4 +125,8 @@ module Mentionable
def cross_reference_exists?(target)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
+
+ def skip_project_check?
+ false
+ end
end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 70740c76e43..4865c0a14b1 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -96,6 +96,11 @@ module Participable
participants.merge(ext.users)
- Ability.users_that_can_read_project(participants.to_a, project)
+ case self
+ when PersonalSnippet
+ Ability.users_that_can_read_personal_snippet(participants.to_a, self)
+ else
+ Ability.users_that_can_read_project(participants.to_a, project)
+ end
end
end
diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb
new file mode 100644
index 00000000000..7b33b837004
--- /dev/null
+++ b/app/models/concerns/presentable.rb
@@ -0,0 +1,7 @@
+module Presentable
+ def present(**attributes)
+ Gitlab::View::Presenter::Factory
+ .new(self, attributes)
+ .fabricate!
+ end
+end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index ebc75100a54..68385dc47eb 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -11,7 +11,7 @@ module Taskable
INCOMPLETE = 'incomplete'.freeze
ITEM_PATTERN = /
^
- (?:\s*[-+*]|(?:\d+\.))? # optional list prefix
+ \s*(?:[-+*]|(?:\d+\.))? # optional list prefix
\s* # optional whitespace prefix
(\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text.
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
new file mode 100644
index 00000000000..040e3a2884e
--- /dev/null
+++ b/app/models/concerns/time_trackable.rb
@@ -0,0 +1,72 @@
+# == TimeTrackable concern
+#
+# Contains functionality related to objects that support time tracking.
+#
+# Used by Issue and MergeRequest.
+#
+
+module TimeTrackable
+ extend ActiveSupport::Concern
+
+ included do
+ attr_reader :time_spent, :time_spent_user
+
+ alias_method :time_spent?, :time_spent
+
+ default_value_for :time_estimate, value: 0, allows_nil: false
+
+ validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
+ validate :check_negative_time_spent
+
+ has_many :timelogs, as: :trackable, dependent: :destroy
+ end
+
+ def spend_time(options)
+ @time_spent = options[:duration]
+ @time_spent_user = options[:user]
+ @original_total_time_spent = nil
+
+ return if @time_spent == 0
+
+ if @time_spent == :reset
+ reset_spent_time
+ else
+ add_or_subtract_spent_time
+ end
+ end
+ alias_method :spend_time=, :spend_time
+
+ def total_time_spent
+ timelogs.sum(:time_spent)
+ end
+
+ def human_total_time_spent
+ Gitlab::TimeTrackingFormatter.output(total_time_spent)
+ end
+
+ def human_time_estimate
+ Gitlab::TimeTrackingFormatter.output(time_estimate)
+ end
+
+ private
+
+ def reset_spent_time
+ timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user)
+ end
+
+ def add_or_subtract_spent_time
+ timelogs.new(time_spent: time_spent, user: @time_spent_user)
+ end
+
+ def check_negative_time_spent
+ return if time_spent.nil? || time_spent == :reset
+
+ # we need to cache the total time spent so multiple calls to #valid?
+ # doesn't give a false error
+ @original_total_time_spent ||= total_time_spent
+
+ if time_spent < 0 && (time_spent.abs > @original_total_time_spent)
+ errors.add(:time_spent, 'Time to subtract exceeds the total time spent')
+ end
+ end
+end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index ba4ee6fcf9d..d2e626c22e8 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -1,62 +1,38 @@
class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze
- def initialize(project, current_user, from:)
+ def initialize(project, options)
@project = project
- @current_user = current_user
- @from = from
- @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
+ @options = options
end
def summary
- @summary ||= Summary.new(@project, @current_user, from: @from)
+ @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
+ from: @options[:from],
+ current_user: @options[:current_user]).data
end
- def permissions(user:)
- Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
+ def stats
+ @stats ||= stats_per_stage
end
- def issue
- @fetcher.calculate_metric(:issue,
- Issue.arel_table[:created_at],
- [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
- Issue::Metrics.arel_table[:first_added_to_board_at]])
+ def no_stats?
+ stats.all? { |hash| hash[:value].nil? }
end
- def plan
- @fetcher.calculate_metric(:plan,
- [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
- Issue::Metrics.arel_table[:first_added_to_board_at]],
- Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
- end
-
- def code
- @fetcher.calculate_metric(:code,
- Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
- MergeRequest.arel_table[:created_at])
- end
-
- def test
- @fetcher.calculate_metric(:test,
- MergeRequest::Metrics.arel_table[:latest_build_started_at],
- MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+ def permissions(user:)
+ Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
- def review
- @fetcher.calculate_metric(:review,
- MergeRequest.arel_table[:created_at],
- MergeRequest::Metrics.arel_table[:merged_at])
+ def [](stage_name)
+ Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end
- def staging
- @fetcher.calculate_metric(:staging,
- MergeRequest::Metrics.arel_table[:merged_at],
- MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
- end
+ private
- def production
- @fetcher.calculate_metric(:production,
- Issue.arel_table[:created_at],
- MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ def stats_per_stage
+ STAGES.map do |stage_name|
+ self[stage_name].as_json
+ end
end
end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
index c9910d8cd09..e69de29bb2d 100644
--- a/app/models/cycle_analytics/summary.rb
+++ b/app/models/cycle_analytics/summary.rb
@@ -1,43 +0,0 @@
-class CycleAnalytics
- class Summary
- def initialize(project, current_user, from:)
- @project = project
- @current_user = current_user
- @from = from
- end
-
- def new_issues
- IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
- end
-
- def commits
- ref = @project.default_branch.presence
- count_commits_for(ref)
- end
-
- def deploys
- @project.deployments.where("created_at > ?", @from).count
- end
-
- private
-
- # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
- # a limit. Since we need a commit count, we _can't_ enforce a limit, so
- # the easiest way forward is to replicate the relevant portions of the
- # `log` function here.
- def count_commits_for(ref)
- return unless ref
-
- repository = @project.repository.raw_repository
- sha = @project.repository.commit(ref).sha
-
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log)
- cmd << '--format=%H'
- cmd << "--after=#{@from.iso8601}"
- cmd << sha
-
- raw_output = IO.popen(cmd) { |io| io.read }
- raw_output.lines.count
- end
- end
-end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
new file mode 100644
index 00000000000..646c1e5ce1a
--- /dev/null
+++ b/app/models/dashboard_milestone.rb
@@ -0,0 +1,5 @@
+class DashboardMilestone < GlobalMilestone
+ def issues_finder_params
+ { authorized_only: true }
+ end
+end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index fa54e3540d0..8867ba0d2ff 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -1,6 +1,10 @@
class GenericCommitStatus < CommitStatus
before_validation :set_default_values
+ validates :target_url, addressable_url: true,
+ length: { maximum: 255 },
+ allow_nil: true
+
# GitHub compatible API
alias_attribute :context, :name
@@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus
def tags
[:external]
end
+
+ def detailed_status(current_user)
+ Gitlab::Ci::Status::External::Factory
+ .new(self, current_user)
+ .fabricate!
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index 8be29c697f1..9c74ca84753 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -4,6 +4,8 @@ class Key < ActiveRecord::Base
include AfterCommitQueue
include Sortable
+ LAST_USED_AT_REFRESH_TIME = 1.day.to_i
+
belongs_to :user
before_validation :generate_fingerprint
@@ -50,7 +52,10 @@ class Key < ActiveRecord::Base
end
def update_last_used_at
- UseKeyWorker.perform_async(self.id)
+ lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME)
+ return unless lease.try_obtain
+
+ UseKeyWorker.perform_async(id)
end
def add_to_shell
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 10251302db8..6753504acff 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -91,6 +91,10 @@ class MergeRequest < ActiveRecord::Base
around_transition do |merge_request, transition, block|
Gitlab::Timeless.timeless(merge_request, &block)
end
+
+ after_transition unchecked: :cannot_be_merged do |merge_request, transition|
+ TodoService.new.merge_request_became_unmergeable(merge_request)
+ end
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
@@ -861,9 +865,11 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
- active_diff_notes.each do |note|
- service.execute(note)
- Gitlab::Timeless.timeless(note, &:save)
+ transaction do
+ active_diff_notes.each do |note|
+ service.execute(note)
+ Gitlab::Timeless.timeless(note, &:save)
+ end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index d41833de66f..dd33975731f 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -130,6 +130,8 @@ class Namespace < ActiveRecord::Base
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ remove_exports!
+
# If repositories moved successfully we need to
# send update instructions to users.
# However we cannot allow rollback since we moved namespace dir
@@ -214,6 +216,8 @@ class Namespace < ActiveRecord::Base
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
end
end
+
+ remove_exports!
end
def refresh_access_of_projects_invited_groups
@@ -226,4 +230,20 @@ class Namespace < ActiveRecord::Base
def full_path_changed?
path_changed? || parent_id_changed?
end
+
+ def remove_exports!
+ Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
+ end
+
+ def export_path
+ File.join(Gitlab::ImportExport.storage_path, full_path_was)
+ end
+
+ def full_path_was
+ if parent
+ parent.full_path + '/' + path_was
+ else
+ path_was
+ end
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 0c1b05dabf2..bf090a0438c 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -43,7 +43,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
- validates :note, :project, presence: true
+ validates :note, presence: true
+ validates :project, presence: true, unless: :for_personal_snippet?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
@@ -53,7 +54,7 @@ class Note < ActiveRecord::Base
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
- validate unless: [:for_commit?, :importing?] do |note|
+ validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
@@ -83,7 +84,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id
- after_save :keep_around_commit
+ after_save :keep_around_commit, unless: :for_personal_snippet?
class << self
def model_name
@@ -165,6 +166,14 @@ class Note < ActiveRecord::Base
noteable_type == "Snippet"
end
+ def for_personal_snippet?
+ noteable.is_a?(PersonalSnippet)
+ end
+
+ def skip_project_check?
+ for_personal_snippet?
+ end
+
# override to return commits, which are not active record
def noteable
if for_commit?
@@ -220,6 +229,10 @@ class Note < ActiveRecord::Base
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
+ def to_ability_name
+ for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
+ end
+
private
def keep_around_commit
diff --git a/app/models/project.rb b/app/models/project.rb
index 1630975b0d3..cd35601d76b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -121,8 +121,6 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
- # Merge requests from source project should be kept when source project was removed
- has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 7c23b766763..3728f5642e4 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -25,7 +25,7 @@ You can create a Personal Access Token here:
http://app.asana.com/-/account_api'
end
- def to_param
+ def self.to_param
'asana'
end
@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api'
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index d839221d315..aeeff8917bf 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -12,7 +12,7 @@ class AssemblaService < Service
'Project Management Software (Source Commits Endpoint)'
end
- def to_param
+ def self.to_param
'assembla'
end
@@ -23,7 +23,7 @@ class AssemblaService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 4819bdbef8c..400020ee04a 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -40,7 +40,7 @@ class BambooService < CiService
'You must set up automatic revision labeling and a repository trigger in Bamboo.'
end
- def to_param
+ def self.to_param
'bamboo'
end
@@ -56,10 +56,6 @@ class BambooService < CiService
]
end
- def supported_events
- %w(push)
- end
-
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 338e685339a..046e2809f45 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'bugzilla'
end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index e77942d8f3c..0956c4a4ede 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -24,10 +24,6 @@ class BuildkiteService < CiService
hook.save
end
- def supported_events
- %w(push)
- end
-
def execute(data)
return unless supported_events.include?(data[:object_kind])
@@ -54,7 +50,7 @@ class BuildkiteService < CiService
'Continuous integration and deployments'
end
- def to_param
+ def self.to_param
'buildkite'
end
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 201b94b065b..ebd21e37189 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -19,11 +19,11 @@ class BuildsEmailService < Service
'Email the builds status to a list of recipients.'
end
- def to_param
+ def self.to_param
'builds_email'
end
- def supported_events
+ def self.supported_events
%w(build)
end
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 5af93860d09..0de59af5652 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -12,7 +12,7 @@ class CampfireService < Service
'Simple web-based real-time group chat'
end
- def to_param
+ def self.to_param
'campfire'
end
@@ -24,7 +24,7 @@ class CampfireService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index b7ef44c3054..8468934425f 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -25,7 +25,7 @@ class ChatNotificationService < Service
valid?
end
- def supported_events
+ def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page]
end
@@ -82,19 +82,19 @@ class ChatNotificationService < Service
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
- PushMessage.new(data)
+ ChatMessage::PushMessage.new(data)
when "issue"
- IssueMessage.new(data) unless is_update?(data)
+ ChatMessage::IssueMessage.new(data) unless is_update?(data)
when "merge_request"
- MergeMessage.new(data) unless is_update?(data)
+ ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note"
- NoteMessage.new(data)
+ ChatMessage::NoteMessage.new(data)
when "build"
- BuildMessage.new(data) if should_build_be_notified?(data)
+ ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
- PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
- WikiPageMessage.new(data)
+ ChatMessage::WikiPageMessage.new(data)
end
end
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 0bc160af604..2bcff541cc0 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service
ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
- def supported_events
- []
+ def self.supported_events
+ %w()
end
def can_test?
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 4de0106707e..82979c8bd34 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -8,7 +8,7 @@ class CiService < Service
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index b2f426dc2ac..dea915a4d05 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'custom_issue_tracker'
end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index ab353a1abe6..91a55514a9a 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -5,8 +5,8 @@
class DeploymentService < Service
default_value_for :category, 'deployment'
- def supported_events
- []
+ def self.supported_events
+ %w()
end
def predefined_variables
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 4bbbebf54cb..0a217d8caba 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -32,7 +32,7 @@ class DroneCiService < CiService
true
end
- def supported_events
+ def self.supported_events
%w(push merge_request tag_push)
end
@@ -87,7 +87,7 @@ class DroneCiService < CiService
'Drone is a Continuous Integration platform built on Docker, written in Go'
end
- def to_param
+ def self.to_param
'drone_ci'
end
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 79285cbd26d..f4f913ee0b6 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -12,11 +12,11 @@ class EmailsOnPushService < Service
'Email the commits and diff of each push to a list of recipients.'
end
- def to_param
+ def self.to_param
'emails_on_push'
end
- def supported_events
+ def self.supported_events
%w(push tag_push)
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index d7b6e505191..bdf6fa6a586 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -13,7 +13,7 @@ class ExternalWikiService < Service
'Replaces the link to the internal wiki with a link to an external wiki.'
end
- def to_param
+ def self.to_param
'external_wiki'
end
@@ -29,4 +29,8 @@ class ExternalWikiService < Service
nil
end
end
+
+ def self.supported_events
+ %w()
+ end
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index dd00275187f..10a13c3fbdc 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -12,7 +12,7 @@ class FlowdockService < Service
'Flowdock is a collaboration web app for technical teams.'
end
- def to_param
+ def self.to_param
'flowdock'
end
@@ -22,7 +22,7 @@ class FlowdockService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 598aca5e06d..f271e1f1739 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -12,7 +12,7 @@ class GemnasiumService < Service
'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.'
end
- def to_param
+ def self.to_param
'gemnasium'
end
@@ -23,7 +23,7 @@ class GemnasiumService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 6bd8d4ec568..ad4eb9536e1 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService
default_value_for :default, true
- def to_param
+ def self.to_param
'gitlab'
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 915f6fed74c..72da219df28 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -27,7 +27,7 @@ class HipchatService < Service
'Private group chat and IM'
end
- def to_param
+ def self.to_param
'hipchat'
end
@@ -45,7 +45,7 @@ class HipchatService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push issue confidential_issue merge_request note tag_push build)
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 7355918feab..5d93064f9b3 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -17,11 +17,11 @@ class IrkerService < Service
'gateway.'
end
- def to_param
+ def self.to_param
'irker'
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index bce2cdd5516..9e65fdbf9d6 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -57,7 +57,7 @@ class IssueTrackerService < Service
end
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2d969d2fcb6..2ac76e97de0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -12,7 +12,7 @@ class JiraService < IssueTrackerService
# This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service
# configuration screen.
- def supported_events
+ def self.supported_events
%w(commit merge_request)
end
@@ -81,7 +81,7 @@ class JiraService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'jira'
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 085125ca9dc..fa3cedc4354 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -52,7 +52,7 @@ class KubernetesService < DeploymentService
'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end
- def to_param
+ def self.to_param
'kubernetes'
end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index ee8a0b55275..4ebc5318da1 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService
'Receive event notifications in Mattermost'
end
- def to_param
+ def self.to_param
'mattermost'
end
@@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService
end
def default_channel_placeholder
- "#town-square"
+ "town-square"
end
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 2cb481182d7..50a011db74e 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Mattermost"
end
- def to_param
+ def self.to_param
'mattermost_slash_commands'
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 745f9bd1b43..ac617f409d9 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -15,11 +15,11 @@ class PipelinesEmailService < Service
'Email the pipelines status to a list of recipients.'
end
- def to_param
+ def self.to_param
'pipelines_email'
end
- def supported_events
+ def self.supported_events
%w[pipeline]
end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index 5301f9fa0ff..9cc642591f4 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -14,7 +14,7 @@ class PivotaltrackerService < Service
'Project Management Software (Source Commits Endpoint)'
end
- def to_param
+ def self.to_param
'pivotaltracker'
end
@@ -34,7 +34,7 @@ class PivotaltrackerService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 3dd878e4c7d..a963d27a376 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -13,7 +13,7 @@ class PushoverService < Service
'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.'
end
- def to_param
+ def self.to_param
'pushover'
end
@@ -61,7 +61,7 @@ class PushoverService < Service
]
end
- def supported_events
+ def self.supported_events
%w(push)
end
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index f9da273cf08..6acf611eba5 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService
end
end
- def to_param
+ def self.to_param
'redmine'
end
end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 76d233a3cca..f77d2d7c60b 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -7,7 +7,7 @@ class SlackService < ChatNotificationService
'Receive event notifications in Slack'
end
- def to_param
+ def self.to_param
'slack'
end
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index 5a7cc0fb329..c34991e4262 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Slack"
end
- def to_param
+ def self.to_param
'slack_slash_commands'
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index 6726082048f..cbaffb8ce48 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -43,14 +43,10 @@ class TeamcityService < CiService
'requests build, that setting is in the vsc root advanced settings.'
end
- def to_param
+ def self.to_param
'teamcity'
end
- def supported_events
- %w(push)
- end
-
def fields
[
{ type: 'text', name: 'teamcity_url',
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 2270ac75071..06abd406523 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -25,8 +25,9 @@ class ProjectStatistics < ActiveRecord::Base
self.commit_count = project.repository.commit_count
end
+ # Repository#size needs to be converted from MB to Byte.
def update_repository_size
- self.repository_size = project.repository.size
+ self.repository_size = project.repository.size * 1.megabyte
end
def update_lfs_objects_size
diff --git a/app/models/service.rb b/app/models/service.rb
index 19ef3ba9c23..043be222f3a 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -76,6 +76,11 @@ class Service < ActiveRecord::Base
def to_param
# implement inside child
+ self.class.to_param
+ end
+
+ def self.to_param
+ raise NotImplementedError
end
def fields
@@ -92,7 +97,11 @@ class Service < ActiveRecord::Base
end
def event_names
- supported_events.map { |event| "#{event}_events" }
+ self.class.event_names
+ end
+
+ def self.event_names
+ self.supported_events.map { |event| "#{event}_events" }
end
def event_field(event)
@@ -104,6 +113,10 @@ class Service < ActiveRecord::Base
end
def supported_events
+ self.class.supported_events
+ end
+
+ def self.supported_events
%w(push tag_push issue confidential_issue merge_request wiki_page)
end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
new file mode 100644
index 00000000000..f768c4e3da5
--- /dev/null
+++ b/app/models/timelog.rb
@@ -0,0 +1,6 @@
+class Timelog < ActiveRecord::Base
+ validates :time_spent, :user, presence: true
+
+ belongs_to :trackable, polymorphic: true
+ belongs_to :user
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f5ade1cc293..4c99aa0d3be 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base
BUILD_FAILED = 3
MARKED = 4
APPROVAL_REQUIRED = 5 # This is an EE-only feature
+ UNMERGEABLE = 6
ACTION_NAMES = {
ASSIGNED => :assigned,
MENTIONED => :mentioned,
BUILD_FAILED => :build_failed,
MARKED => :marked,
- APPROVAL_REQUIRED => :approval_required
+ APPROVAL_REQUIRED => :approval_required,
+ UNMERGEABLE => :unmergeable
}
belongs_to :author, class_name: "User"
@@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base
end
end
+ def unmergeable?
+ action == UNMERGEABLE
+ end
+
def build_failed?
action == BUILD_FAILED
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 06dd98a3188..2caa66dd9f7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -179,8 +179,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
- scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
- scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
+ scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 118c100ca11..b9f1c29c32e 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -53,6 +53,10 @@ class BasePolicy
def self.class_for(subject)
return GlobalPolicy if subject.nil?
+ if subject.class.try(:presenter?)
+ subject = subject.subject
+ end
+
subject.class.ancestors.each do |klass|
next unless klass.name
diff --git a/app/presenters/README.md b/app/presenters/README.md
new file mode 100644
index 00000000000..3edd63451e7
--- /dev/null
+++ b/app/presenters/README.md
@@ -0,0 +1,154 @@
+# Presenters
+
+This type of class is responsible for giving the view an object which defines
+**view-related logic/data methods**. It is usually useful to extract such
+methods from models to presenters.
+
+## When to use a presenter?
+
+### When your view is full of logic
+
+When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's
+time to create a presenter!
+
+### When your model has a lot of view-related logic/data methods
+
+When your model has a lot of view-related logic/data methods, you can easily
+move them to a presenter.
+
+## Why are we using presenters instead of helpers?
+
+We don't use presenters to generate complex view output that would rely on helpers.
+
+Presenters should be used for:
+
+- Data and logic methods that can be pulled & combined into single methods from
+ view. This can include loops extracted from views too. A good example is
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7073/diffs.
+- Data and logic methods that can be pulled from models.
+- Simple text output methods: it's ok if the method returns a string, but not a
+ whole DOM element for which we'd need HAML, a view context, helpers etc.
+
+## Why use presenters instead of model concerns?
+
+We should strive to follow the single-responsibility principle, and view-related
+logic/data methods are definitely not the responsibility of models!
+
+Another reason is as follows:
+
+> Avoid using concerns and use presenters instead. Why? After all, concerns seem
+to be a core part of Rails and can DRY up code when shared among multiple models.
+Nonetheless, the main issue is that concerns don’t make the model object more
+cohesive. The code is just better organized. In other words, there’s no real
+change to the API of the model.
+
+– https://www.toptal.com/ruby-on-rails/decoupling-rails-components
+
+## Benefits
+
+By moving pure view-related logic/data methods from models & views to presenters,
+we gain the following benefits:
+
+- rules are more explicit and centralized in the presenter => improves security
+- testing is easier and faster as presenters are Plain Old Ruby Object (PORO)
+- views are more readable and maintainable
+- decreases number of CE -> EE merge conflicts since code is in separate files
+- moves the conflicts from views (not always obvious) to presenters (a lot easier to resolve)
+
+## What not to do with presenters?
+
+- Don't use helpers in presenters. Presenters are not aware of the view context.
+- Don't generate complex DOM elements, forms etc. with presenters. Presenters
+ can return simple data as texts, and URLs using URL helpers from
+ `Gitlab::Routing` but nothing much more fancy.
+
+## Implementation
+
+### Presenter definition
+
+Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
+provides a `.presents` method which allows you to define an accessor for the
+presented object. It also includes common helpers like `Gitlab::Routing` and
+`Gitlab::Allowable`.
+
+```ruby
+class LabelPresenter < Gitlab::View::Presenter::Simple
+ presents :label
+
+ def text_color
+ label.color.to_s
+ end
+
+ def to_partial_path
+ 'projects/labels/show'
+ end
+end
+```
+
+In some cases, it can be more practical to transparently delegate all missing
+method calls to the presented object, in these cases, you can make your
+presenter inherit from `Gitlab::View::Presenter::Delegated`:
+
+```ruby
+class LabelPresenter < Gitlab::View::Presenter::Delegated
+ presents :label
+
+ def text_color
+ # color is delegated to label
+ color.to_s
+ end
+
+ def to_partial_path
+ 'projects/labels/show'
+ end
+end
+```
+
+### Presenter instantiation
+
+Instantiation must be done via the `Gitlab::View::Presenter::Factory` class which
+detects the presenter based on the presented subject's class.
+
+```ruby
+class Projects::LabelsController < Projects::ApplicationController
+ def edit
+ @label = Gitlab::View::Presenter::Factory
+ .new(@label, user: current_user)
+ .fabricate!
+ end
+end
+```
+
+You can also include the `Presentable` concern in the model:
+
+```ruby
+class Label
+ include Presentable
+end
+```
+
+and then in the controller:
+
+```ruby
+class Projects::LabelsController < Projects::ApplicationController
+ def edit
+ @label = @label.present(user: current_user)
+ end
+end
+```
+
+### Presenter usage
+
+```ruby
+%div{ class: @label.text_color }
+ = render partial: @label, label: @label
+```
+
+You can also present the model in the view:
+
+```ruby
+- label = @label.present(current_user)
+
+%div{ class: label.text_color }
+ = render partial: label, label: label
+```
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
new file mode 100644
index 00000000000..ed72ed14d72
--- /dev/null
+++ b/app/presenters/ci/build_presenter.rb
@@ -0,0 +1,15 @@
+module Ci
+ class BuildPresenter < Gitlab::View::Presenter::Delegated
+ presents :build
+
+ def erased_by_user?
+ # Build can be erased through API, therefore it does not have
+ # `erased_by` user assigned in that case.
+ erased? && erased_by
+ end
+
+ def erased_by_name
+ erased_by.name if erased_by_user?
+ end
+ end
+end
diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
new file mode 100644
index 00000000000..a559d0850c4
--- /dev/null
+++ b/app/serializers/analytics_stage_entity.rb
@@ -0,0 +1,10 @@
+class AnalyticsStageEntity < Grape::Entity
+ include EntityDateHelper
+
+ expose :title
+ expose :description
+
+ expose :median, as: :value do |stage|
+ stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
+ end
+end
diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb
new file mode 100644
index 00000000000..613cf6874d8
--- /dev/null
+++ b/app/serializers/analytics_stage_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsStageSerializer < BaseSerializer
+ entity AnalyticsStageEntity
+end
diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb
new file mode 100644
index 00000000000..91803ec07f5
--- /dev/null
+++ b/app/serializers/analytics_summary_entity.rb
@@ -0,0 +1,7 @@
+class AnalyticsSummaryEntity < Grape::Entity
+ expose :value, safe: true
+
+ expose :title do |object|
+ object.title.pluralize(object.value)
+ end
+end
diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb
new file mode 100644
index 00000000000..c87a24aa47c
--- /dev/null
+++ b/app/serializers/analytics_summary_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsSummarySerializer < BaseSerializer
+ entity AnalyticsSummaryEntity
+end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index 17c9160cb19..29aecb50849 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :deleted_at
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index d04a4990cb0..61f0f11d7d2 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -40,10 +40,12 @@ class PipelineEntity < Grape::Entity
end
expose :path do |pipeline|
- namespace_project_tree_path(
- pipeline.project.namespace,
- pipeline.project,
- id: pipeline.ref)
+ if pipeline.ref
+ namespace_project_tree_path(
+ pipeline.project.namespace,
+ pipeline.project,
+ id: pipeline.ref)
+ end
end
expose :tag?, as: :tag
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
new file mode 100644
index 00000000000..152c8ae5006
--- /dev/null
+++ b/app/services/ci/update_build_queue_service.rb
@@ -0,0 +1,19 @@
+module Ci
+ class UpdateBuildQueueService
+ def execute(build)
+ build.project.runners.each do |runner|
+ if runner.can_pick?(build)
+ runner.tick_runner_queue
+ end
+ end
+
+ return unless build.project.shared_runners_enabled?
+
+ Ci::Runner.shared.each do |runner|
+ if runner.can_pick?(build)
+ runner.tick_runner_queue
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 4ce5fd993d9..5f3ced49665 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -36,6 +36,14 @@ class IssuableBaseService < BaseService
end
end
+ def create_time_estimate_note(issuable)
+ SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
+ end
+
+ def create_time_spent_note(issuable)
+ SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
+ end
+
def filter_params(issuable)
ability_name = :"admin_#{issuable.to_ability_name}"
@@ -272,6 +280,14 @@ class IssuableBaseService < BaseService
create_task_status_note(issuable)
end
+ if issuable.previous_changes.include?('time_estimate')
+ create_time_estimate_note(issuable)
+ end
+
+ if issuable.time_spent?
+ create_time_spent_note(issuable)
+ end
+
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 70e25956dc7..5a53b973059 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -38,15 +38,13 @@ module MergeRequests
private
- def merge_requests_for(branch)
- origin_merge_requests = @project.origin_merge_requests
- .opened.where(source_branch: branch).to_a
-
- fork_merge_requests = @project.fork_merge_requests
- .opened.where(source_branch: branch).to_a
-
- (origin_merge_requests + fork_merge_requests)
- .uniq.select(&:source_project)
+ # Returns all origin and fork merge requests from `@project` satisfying passed arguments.
+ def merge_requests_for(source_branch, mr_states: [:opened])
+ MergeRequest
+ .with_state(mr_states)
+ .where(source_branch: source_branch, source_project_id: @project.id)
+ .preload(:source_project) # we don't need a #includes since we're just preloading for the #select
+ .select(&:source_project)
end
def pipeline_merge_requests(pipeline)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 0a9563ed7e7..b4bfb0e5e8c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -21,6 +21,7 @@ module MergeRequests
end
comment_mr_with_commits
+ mark_mr_as_wip_from_commits
execute_mr_web_hooks
true
@@ -41,7 +42,7 @@ module MergeRequests
commit_ids.include?(merge_request.diff_head_sha)
end
- merge_requests.uniq.select(&:source_project).each do |merge_request|
+ filter_merge_requests(merge_requests).each do |merge_request|
MergeRequests::PostMergeService.
new(merge_request.target_project, @current_user).
execute(merge_request)
@@ -57,10 +58,13 @@ module MergeRequests
def reload_merge_requests
merge_requests = @project.merge_requests.opened.
by_source_or_target_branch(@branch_name).to_a
- merge_requests += fork_merge_requests
- merge_requests = filter_merge_requests(merge_requests)
- merge_requests.each do |merge_request|
+ # Fork merge requests
+ merge_requests += MergeRequest.opened
+ .where(source_branch: @branch_name, source_project: @project)
+ .where.not(target_project: @project).to_a
+
+ filter_merge_requests(merge_requests).each do |merge_request|
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
@@ -136,6 +140,24 @@ module MergeRequests
end
end
+ def mark_mr_as_wip_from_commits
+ return unless @commits.present?
+
+ merge_requests_for_source_branch.each do |merge_request|
+ wip_commit = @commits.detect(&:work_in_progress?)
+
+ if wip_commit && !merge_request.work_in_progress?
+ merge_request.update(title: merge_request.wip_title)
+ SystemNoteService.add_merge_request_wip_from_commit(
+ merge_request,
+ merge_request.project,
+ @current_user,
+ wip_commit
+ )
+ end
+ end
+ end
+
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
@@ -156,16 +178,7 @@ module MergeRequests
end
def merge_requests_for_source_branch
- @source_merge_requests ||= begin
- merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += fork_merge_requests
- filter_merge_requests(merge_requests)
- end
- end
-
- def fork_merge_requests
- @fork_merge_requests ||= @project.fork_merge_requests.opened.
- where(source_branch: @branch_name).to_a
+ @source_merge_requests ||= merge_requests_for(@branch_name)
end
def branch_added?
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index cdd765c85eb..b4f8b33d564 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -3,9 +3,10 @@ module Notes
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
- note = project.notes.new(params)
- note.author = current_user
- note.system = false
+ note = Note.new(params)
+ note.project = project
+ note.author = current_user
+ note.system = false
if note.award_emoji?
noteable = note.noteable
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index e4cd3fc7833..6a10e172483 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -10,6 +10,9 @@ module Notes
# Skip system notes, like status changes and cross-references and awards
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
+
+ return if @note.for_personal_snippet?
+
@note.create_cross_references!
execute_note_hooks
end
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index aaea9717fc4..56913568cae 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -12,7 +12,7 @@ module Notes
def self.supported?(note, current_user)
noteable_update_service(note) &&
current_user &&
- current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
+ current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
end
def supported?(note)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index c3b61e68eab..f74e6cac174 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -178,8 +178,15 @@ class NotificationService
recipients = []
mentioned_users = note.mentioned_users
+
+ ability, subject = if note.for_personal_snippet?
+ [:read_personal_snippet, note.noteable]
+ else
+ [:read_project, note.project]
+ end
+
mentioned_users.select! do |user|
- user.can?(:read_project, note.project)
+ user.can?(ability, subject)
end
# Add all users participating in the thread (author, assignee, comment authors)
@@ -192,11 +199,13 @@ class NotificationService
recipients = recipients.concat(participants)
- # Merge project watchers
- recipients = add_project_watchers(recipients, note.project)
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ recipients = add_project_watchers(recipients, note.project)
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, note.project, :new_note)
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, note.project, :new_note)
+ end
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
@@ -211,8 +220,7 @@ class NotificationService
recipients.delete(note.author)
recipients = recipients.uniq
- # build notify method like 'note_commit_email'
- notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
+ notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index d18844ac0e3..3566a8ba92f 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -255,6 +255,50 @@ module SlashCommands
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
end
+ desc 'Set time estimate'
+ params '<1w 3d 2h 14m>'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :estimate do |raw_duration|
+ time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration)
+
+ if time_estimate
+ @updates[:time_estimate] = time_estimate
+ end
+ end
+
+ desc 'Add or substract spent time'
+ params '<1h 30m | -1h 30m>'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
+ end
+ command :spend do |raw_duration|
+ time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration)
+
+ if time_spent
+ @updates[:spend_time] = { duration: time_spent, user: current_user }
+ end
+ end
+
+ desc 'Remove time estimate'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :remove_estimate do
+ @updates[:time_estimate] = 0
+ end
+
+ desc 'Remove spent time'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :remove_time_spent do
+ @updates[:spend_time] = { duration: :reset, user: current_user }
+ end
+
# This is a dummy command, so that it appears in the autocomplete commands
desc 'CC'
params '@user'
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 7613ecd5021..a11bca00687 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -109,6 +109,57 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ # Called when the estimated time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_estimate - Estimated time
+ #
+ # Example Note text:
+ #
+ # "Changed estimate of this issue to 3d 5h"
+ #
+ # Returns the created Note object
+
+ def change_time_estimate(noteable, project, author)
+ parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
+ body = if noteable.time_estimate == 0
+ "Removed time estimate on this #{noteable.human_class_name}"
+ else
+ "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}"
+ end
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ # Called when the spent time of a Noteable is changed
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # time_spent - Spent time
+ #
+ # Example Note text:
+ #
+ # "Added 2h 30m of time spent on this issue"
+ #
+ # Returns the created Note object
+
+ def change_time_spent(noteable, project, author)
+ time_spent = noteable.time_spent
+
+ if time_spent == :reset
+ body = "Removed time spent on this #{noteable.human_class_name}"
+ else
+ parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
+ action = time_spent > 0 ? 'Added' : 'Subtracted'
+ body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}"
+ end
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
# Called when the status of a Noteable is changed
#
# noteable - Noteable object
@@ -157,6 +208,12 @@ module SystemNoteService
create_note(noteable: noteable, project: project, author: author, note: body)
end
+ def add_merge_request_wip_from_commit(noteable, project, author, commit)
+ body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
def self.resolve_all_discussions(merge_request, project, author)
body = "resolved all discussions"
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index f8e6b2ef094..1bd6ce416ab 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -98,10 +98,12 @@ class TodoService
# When a build fails on the HEAD of a merge request we should:
#
- # * create a todo for that user to fix it
+ # * create a todo for author of MR to fix it
+ # * create a todo for merge_user to keep an eye on it
#
def merge_request_build_failed(merge_request)
- create_build_failed_todo(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_build_succeeds?
end
# When a new commit is pushed to a merge request we should:
@@ -115,11 +117,21 @@ 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
#
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_build_succeeds?
end
-
+
+ # When a merge request could not be automatically merged due to its unmergeable state we should:
+ #
+ # * create a todo for a merge_user
+ #
+ def merge_request_became_unmergeable(merge_request)
+ create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds?
+ end
+
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
@@ -236,10 +248,14 @@ class TodoService
create_todos(mentioned_users, attributes)
end
- def create_build_failed_todo(merge_request)
- author = merge_request.author
- attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED)
- create_todos(author, attributes)
+ def create_build_failed_todo(merge_request, todo_author)
+ attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
+ 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)
end
def attributes_for_target(target)
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 9849b31d7e2..9d7bcdb9d16 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -3,7 +3,7 @@
.todo-item.todo-block
.todo-title.title
- - unless todo.build_failed?
+ - unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo)
%span.author-name
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 2bce2780484..6f5d4bf2a2f 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,6 +1,9 @@
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
+ .timeline-icon
+ = link_to user_path(discussion.author) do
+ = image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 0fb2bb460cb..c6df66d2c3c 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -8,14 +8,10 @@
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
Deploy Keys
- = nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do
+ = nav_link(controller: :integrations) do
+ = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span
- Webhooks
- = nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
- %span
- Services
+ Integrations
= nav_link(controller: :protected_branches) do
= link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
%span
diff --git a/app/views/notify/note_personal_snippet_email.html.haml b/app/views/notify/note_personal_snippet_email.html.haml
new file mode 100644
index 00000000000..2fa2f784661
--- /dev/null
+++ b/app/views/notify/note_personal_snippet_email.html.haml
@@ -0,0 +1 @@
+= render 'note_message'
diff --git a/app/views/notify/note_personal_snippet_email.text.erb b/app/views/notify/note_personal_snippet_email.text.erb
new file mode 100644
index 00000000000..b2a8809a23b
--- /dev/null
+++ b/app/views/notify/note_personal_snippet_email.text.erb
@@ -0,0 +1,8 @@
+New comment for Snippet <%= @snippet.id %>
+
+<%= url_for(snippet_url(@snippet, anchor: "note_#{@note.id}")) %>
+
+
+Author: <%= @note.author_name %>
+
+<%= @note.note %>
diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml
new file mode 100644
index 00000000000..65fc0a36ca9
--- /dev/null
+++ b/app/views/projects/_visibility_select.html.haml
@@ -0,0 +1,7 @@
+- if can_change_visibility_level?(@project, current_user)
+ = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select')
+- else
+ .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } }
+ = visibility_level_icon(@project.visibility_level)
+ %strong
+ = visibility_level_label(@project.visibility_level)
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 54724ef5cab..c613e473e4c 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -51,8 +51,10 @@
.prepend-top-default
- if @build.erased?
.erased.alert.alert-warning
- - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by
- Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)}
+ - if @build.erased_by_user?
+ Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
+ - else
+ Build has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
.scroll-step
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 990bfbcf951..dfdaeb04869 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -78,7 +78,7 @@
.btn-group.inline
- if actions.any?
.btn-group
- %button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Manual build' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -89,7 +89,7 @@
%span= build.name
- if artifacts.present?
.btn-group
- %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -102,8 +102,8 @@
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
- = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+ = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
- = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+ = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Cancel' , method: :post do
= icon("remove")
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 990908211de..421b3db342d 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -13,7 +13,7 @@
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
- = form_tag [type.underscore, @project.namespace, @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
+ = 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 'target_branch', target_label, class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index 13ab2253733..8aed88da38b 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -7,4 +7,4 @@
= nav_link(path: 'commit#pipelines') do
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines
- %span.badge= @ci_pipelines.count
+ %span.badge= @commit.pipelines.size
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index a9ee9230076..08eb0c57f66 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,7 +1,7 @@
.page-content-header
.header-main-content
%strong
- = clipboard_button(clipboard_text: @commit.id)
+ = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
= @commit.short_id
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index 8233e26e4e7..00e7cdd1729 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -3,4 +3,4 @@
= render "commit_box"
= render "ci_menu"
-= render "pipelines_list", pipelines: @ci_pipelines
+= render "pipelines_list", pipelines: @commit.pipelines.order(id: :desc)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 3c6c50dce3c..002e3d345dc 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -36,6 +36,6 @@
.table-list-cell.commit-actions.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
- = clipboard_button(clipboard_text: commit.id)
+ = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index e77f23c7fd8..d94f23f5a38 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -9,10 +9,13 @@
= render "head"
%div{ class: container_class }
- .row-content-block.second-block.content-component-block
+ .row-content-block.second-block.content-component-block.flex-container-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
@@ -30,8 +33,6 @@
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
= icon("rss")
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
%div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 38e7fc4279c..1d7fdf68cb3 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -21,76 +21,68 @@
.form-group
= f.label :default_branch, "Default Branch", class: 'label-light'
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
- .form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'label-light' do
- Visibility Level
- = link_to "(?)", help_page_path("public_access/public_access")
- - if can_change_visibility_level?(@project, current_user)
- = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
- - else
- .info
- = visibility_level_icon(@project.visibility_level)
- %strong
- = visibility_level_label(@project.visibility_level)
- .light= visibility_level_description(@project.visibility_level, @project)
-
- .form-group
- = render 'shared/allow_request_access', form: f
-
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%hr
- %fieldset.features.append-bottom-0
+ %fieldset.append-bottom-0
%h5.prepend-top-0
- Feature Visibility
-
- = f.fields_for :project_feature do |feature_fields|
- .form_group.prepend-top-20
- .row
- .col-md-9
- = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
- %span.help-block Push files to be stored in this project
- .col-md-3.js-repo-access-level
- = project_feature_access_select(:repository_access_level)
-
- .col-sm-12
- .row
- .col-md-9.project-feature-nested
- = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
- %span.help-block Submit changes to be merged upstream
- .col-md-3
- = project_feature_access_select(:merge_requests_access_level)
+ Sharing &amp; Permissions
+ .form_group.prepend-top-20.sharing-and-permissions
+ .row.js-visibility-select
+ .col-md-9
+ %label.label-light
+ = label_tag :project_visibility, 'Project Visibility', class: 'label-light'
+ = link_to "(?)", help_page_path("public_access/public_access")
+ %span.help-block
+ .col-md-3.visibility-select-container
+ = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
+ = f.fields_for :project_feature do |feature_fields|
+ %fieldset.features
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
+ %span.help-block View and edit files in this project
+ .col-md-3.js-repo-access-level
+ = project_feature_access_select(:repository_access_level)
- .row
- .col-md-9.project-feature-nested
- = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
- %span.help-block Submit, test and deploy your changes before merge
- .col-md-3
- = project_feature_access_select(:builds_access_level)
+ .row
+ .col-md-9.project-feature.nested
+ = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
+ %span.help-block Submit changes to be merged upstream
+ .col-md-3
+ = project_feature_access_select(:merge_requests_access_level)
- .row
- .col-md-9
- = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
- %span.help-block Share code pastes with others out of Git repository
- .col-md-3
- = project_feature_access_select(:snippets_access_level)
+ .row
+ .col-md-9.project-feature.nested
+ = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
+ %span.help-block Submit, test and deploy your changes before merge
+ .col-md-3
+ = project_feature_access_select(:builds_access_level)
- .row
- .col-md-9
- = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
- %span.help-block Lightweight issue tracking system for this project
- .col-md-3
- = project_feature_access_select(:issues_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
+ %span.help-block Share code pastes with others out of Git repository
+ .col-md-3
+ = project_feature_access_select(:snippets_access_level)
- .row
- .col-md-9
- = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
- %span.help-block Pages for project documentation
- .col-md-3
- = project_feature_access_select(:wiki_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
+ %span.help-block Lightweight issue tracking system for this project
+ .col-md-3
+ = project_feature_access_select(:issues_access_level)
+ .row
+ .col-md-9.project-feature
+ = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
+ %span.help-block Pages for project documentation
+ .col-md-3
+ = project_feature_access_select(:wiki_access_level)
+ .form-group
+ = render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
.row
.col-md-9
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/_index.html.haml
index 8faad351463..8faad351463 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 43141971231..9fa00811af0 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -2,6 +2,8 @@
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/vue_resource.js')
.clearfix.detail-page-header
.issuable-header
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 34ead6427e0..36c6e7a8dad 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -11,7 +11,7 @@
= 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|
- = render 'shared/issuable/form', f: f, issuable: @merge_request
+ = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
= f.hidden_field :target_project_id
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 0e14b9d4bf4..2a7cd3a19d0 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -3,6 +3,7 @@
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/vue_resource.js')
= page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
.merge-request{ 'data-url' => merge_request_path(@merge_request) }
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index b8b87dcdcaf..ebef2157d34 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,5 +1,6 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/vue_resource.js')
= page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title"
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
index 072d01d144e..f70cd09c5f4 100644
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+
%h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the pipeline succeeds.
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/_index.html.haml
index 66fd3029dc9..964133504e6 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -1,5 +1,3 @@
-- page_title "Services"
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
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 63b797cd391..c1e576b42fc 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -8,8 +8,8 @@
by entering
%code /&lt;command_trigger_word&gt; help
- - unless enabled
+ - unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
-- if enabled
+- if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
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 6d7c2defe2b..04b9100acc6 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,4 +1,5 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
+- run_actions_text = "Perform common operations on this project: #{pretty_name}"
.well
This service allows GitLab users to perform common operations on this
@@ -9,85 +10,86 @@
%code /&lt;command&gt; help
%br
%br
- To setup this service:
- %ul.list-unstyled
- %li
- 1.
- = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
- in your Slack team with these options:
+ - unless @service.template?
+ To setup this service:
+ %ul.list-unstyled
+ %li
+ 1.
+ = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
+ in your Slack team with these options:
- %hr
+ %hr
- .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
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.path_with_namespace
+ .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
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.path_with_namespace
- .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(clipboard_target: '#url')
+ .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(clipboard_target: '#url')
- .form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-12.text-block POST
+ .form-group
+ = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-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(clipboard_target: '#customize_name')
+ .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(clipboard_target: '#customize_name')
- .form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
- .col-sm-10.col-xs-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')
+ .form-group
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
+ .col-sm-10.col-xs-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')
- .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
+ .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
- .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(clipboard_target: '#autocomplete_description')
+ .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(clipboard_target: '#autocomplete_description')
- .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(clipboard_target: '#autocomplete_usage_hint')
+ .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(clipboard_target: '#autocomplete_usage_hint')
- .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(clipboard_target: '#descriptive_label')
+ .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(clipboard_target: '#descriptive_label')
- %hr
+ %hr
- %ul.list-unstyled
- %li
- 2. Paste the
- %strong Token
- into the field below
- %li
- 3. Select the
- %strong Active
- checkbox, press
- %strong Save changes
- and start using GitLab inside Slack!
+ %ul.list-unstyled
+ %li
+ 2. Paste the
+ %strong Token
+ into the field below
+ %li
+ 3. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Slack!
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index ceabe2eab3d..ceabe2eab3d 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
new file mode 100644
index 00000000000..aa38a889cdd
--- /dev/null
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -0,0 +1,3 @@
+- page_title 'Integrations'
+= render 'projects/hooks/index'
+= render 'projects/services/index'
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 038a960bd0c..2c08221565b 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -8,7 +8,7 @@
.pull-left Last commit
.last-commit.hidden-sm.pull-left
%small.light
- = clipboard_button(clipboard_text: @commit.id)
+ = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard")
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
= time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index ee043910548..94295970acf 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.choose-btn.btn.btn-sm.js-choose-group-avatar-button
+%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' }
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg
new file mode 100644
index 00000000000..f20de04538e
--- /dev/null
+++ b/app/views/shared/icons/_icon_stopwatch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index dcc1f3ba676..c0e8a498316 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,4 +1,5 @@
- form = local_assigns.fetch(:f)
+- commits = local_assigns[:commits]
- project = @target_project || @project
= form_errors(issuable)
@@ -14,7 +15,7 @@
= form.label :title, class: 'control-label'
= render 'shared/issuable/form/template_selector', issuable: issuable
- = render 'shared/issuable/form/title', issuable: issuable, form: form
+ = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
= render 'shared/issuable/form/description', issuable: issuable, form: form
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 8d7b1d616f4..e9644ca0f12 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,13 +11,13 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id }
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true }
- %li.filter-dropdown-item{ 'data-value' => '' }
+ %li.filter-dropdown-item{ 'data-action' => 'submit' }
%button.btn.btn-link
= icon('search')
%span
@@ -47,6 +47,10 @@
%li.filter-dropdown-item{ 'data-value' => 'none' }
%button.btn.btn-link
No Assignee
+ - if current_user
+ %li.filter-dropdown-item{ 'data-value' => current_user.to_reference }
+ %button.btn.btn-link
+ Assigned to me
%li.divider
%ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
%li.filter-dropdown-item
@@ -121,7 +125,13 @@
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
- $('form.filter-form').on('submit', function (event) {
- event.preventDefault();
- Turbolinks.visit(this.action + '&' + $(this).serialize());
+
+ $(document).off('page:restore').on('page:restore', function (event) {
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
+ Issuable.init();
+ new gl.IssuableBulkActions({
+ prefixId: 'issue_',
+ });
});
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index a02b815e3cd..ec9bcaf63dd 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,5 +1,7 @@
- todo = issuable_todo(issuable)
-%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('issuable/issuable_bundle.js')
+%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
@@ -72,7 +74,13 @@
.selectbox.hide-collapsed
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
-
+ - if issuable.has_attribute?(:time_estimate)
+ #issuable-time-tracker.block
+ %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') }
+ // Fallback while content is loading
+ .title.hide-collapsed
+ Time tracking
+ = icon('spinner spin')
- if issuable.has_attribute?(:due_date)
.block.due_date
.sidebar-collapsed-icon
@@ -162,6 +170,8 @@
= clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left")
:javascript
+ gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
+ new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 83efdc7c8f7..64826d41d60 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -1,4 +1,5 @@
- issuable = local_assigns.fetch(:issuable)
+- has_wip_commits = local_assigns.fetch(:has_wip_commits)
- form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(issuable).empty?
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
@@ -18,6 +19,9 @@
%strong Work In Progress
merge request to be merged when it's ready.
.js-no-wip-explanation
+ - if has_wip_commits
+ It looks like you have some WIP commits in this branch.
+ %br
%a.js-toggle-wip{ href: '', tabindex: -1 }
Start the title with
%code WIP:
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 51c195ffbcd..28935c8b598 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -16,7 +16,7 @@
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
.issuable-detail
= link_to [project.namespace.becomes(Namespace), project, issuable] do
- %span.issuable-number >= issuable.to_reference
+ %span.issuable-number= issuable.to_reference
- issuable.labels.each do |label|
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index c8fd45c4319..31eb07ca666 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -8,8 +8,7 @@
= title
- if show_counter
.right
- = issuables.size
- .pull-right= number_with_delimiter(issuables.size)
+ = number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 5d659eb83a9..13586a5a12a 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,6 +1,3 @@
-- page_title "Webhooks"
-- context_title = @project ? 'project' : 'group'
-
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
new file mode 100644
index 00000000000..fa9e097e40a
--- /dev/null
+++ b/app/workers/build_queue_worker.rb
@@ -0,0 +1,10 @@
+class BuildQueueWorker
+ include Sidekiq::Worker
+ include BuildQueue
+
+ def perform(build_id)
+ Ci::Build.find_by(id: build_id).try do |build|
+ Ci::UpdateBuildQueueService.new.execute(build)
+ end
+ end
+end
diff --git a/changelogs/unreleased/18786-go-to-a-project-order.yml b/changelogs/unreleased/18786-go-to-a-project-order.yml
deleted file mode 100644
index 1b9e246d1a7..00000000000
--- a/changelogs/unreleased/18786-go-to-a-project-order.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Go to a project order
-merge_request: 7737
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/19086-double-newline.yml b/changelogs/unreleased/19086-double-newline.yml
deleted file mode 100644
index dd9b58920fb..00000000000
--- a/changelogs/unreleased/19086-double-newline.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix double spaced CI log
-merge_request: 8349
-author: Jared Deckard <jared.deckard@gmail.com>
diff --git a/changelogs/unreleased/19966-api-call-to-move-project-to-different-group-fails-when-using-group-and-project-names-instead-of-id.yml b/changelogs/unreleased/19966-api-call-to-move-project-to-different-group-fails-when-using-group-and-project-names-instead-of-id.yml
deleted file mode 100644
index 61cf047026b..00000000000
--- a/changelogs/unreleased/19966-api-call-to-move-project-to-different-group-fails-when-using-group-and-project-names-instead-of-id.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow group and project paths when transferring projects via the API
-merge_request:
-author:
diff --git a/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml b/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml
deleted file mode 100644
index 5570ede4a9a..00000000000
--- a/changelogs/unreleased/19988-prevent-empty-pagination-when-list-not-empty.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent empty pagination when list is not empty
-merge_request: 8172
-author:
diff --git a/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml b/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml
deleted file mode 100644
index 574c322803c..00000000000
--- a/changelogs/unreleased/21135-resolve-these-conflicts-link-is-too-subtle.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve visibility of "Resolve conflicts" and "Merge locally" actions
-merge_request: 8229
-author:
diff --git a/changelogs/unreleased/22111-remove-lock-icon-on-protected-tag.yml b/changelogs/unreleased/22111-remove-lock-icon-on-protected-tag.yml
deleted file mode 100644
index e4f7c1b7762..00000000000
--- a/changelogs/unreleased/22111-remove-lock-icon-on-protected-tag.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove Lock Icon on Protected Tag
-merge_request: 8513
-author: Sergey Nikitin
diff --git a/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email
new file mode 100644
index 00000000000..f4011b756a5
--- /dev/null
+++ b/changelogs/unreleased/22619-add-an-email-address-to-unsubscribe-list-header-in-email
@@ -0,0 +1,4 @@
+---
+title: Handle unsubscribe from email notifications via replying to reply+%{key}+unsubscribe@ address
+merge_request: 6597
+author:
diff --git a/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
new file mode 100644
index 00000000000..2c6883bcf7b
--- /dev/null
+++ b/changelogs/unreleased/22638-creating-a-branch-matching-a-wildcard-fails.yml
@@ -0,0 +1,4 @@
+---
+title: Allow creating protected branches when user can merge to such branch
+merge_request: 8458
+author:
diff --git a/changelogs/unreleased/22974-trigger-service-events-through-api.yml b/changelogs/unreleased/22974-trigger-service-events-through-api.yml
new file mode 100644
index 00000000000..57106e8c676
--- /dev/null
+++ b/changelogs/unreleased/22974-trigger-service-events-through-api.yml
@@ -0,0 +1,4 @@
+---
+title: Adds service trigger events to api
+merge_request: 8324
+author:
diff --git a/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
new file mode 100644
index 00000000000..268be6b9b83
--- /dev/null
+++ b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml
@@ -0,0 +1,4 @@
+---
+title: Create a TODO for user who set auto-merge when a build fails, merge conflict occurs
+merge_request: 8056
+author: twonegatives
diff --git a/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml b/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml
deleted file mode 100644
index 83cf3670ec0..00000000000
--- a/changelogs/unreleased/24139-production-wildcard-for-cycle-analytics.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Treat environments matching `production/*` as Production
-merge_request: 8500
-author:
diff --git a/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml b/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml
deleted file mode 100644
index 09ff63a44fb..00000000000
--- a/changelogs/unreleased/24185-legacy-ci-status-reactive-cache.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Query external CI statuses in the background
-merge_request:
-author:
diff --git a/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml b/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml
deleted file mode 100644
index fac7697ede1..00000000000
--- a/changelogs/unreleased/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix button layout issue on branches page
-merge_request: 8074
-author:
diff --git a/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
new file mode 100644
index 00000000000..be66c370f36
--- /dev/null
+++ b/changelogs/unreleased/24833-Allow-to-search-by-commit-hash-within-project.yml
@@ -0,0 +1,4 @@
+---
+title: 'Allows to search within project by commit hash'
+merge_request:
+author: YarNayar
diff --git a/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml b/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml
deleted file mode 100644
index c31c89dc4bc..00000000000
--- a/changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: ensure permalinks scroll to correct position on multiple clicks
-merge_request: 8046
-author:
diff --git a/changelogs/unreleased/24915_merge_slash_command.yml b/changelogs/unreleased/24915_merge_slash_command.yml
deleted file mode 100644
index eb8ced8ab01..00000000000
--- a/changelogs/unreleased/24915_merge_slash_command.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support slash comand `/merge` for merging merge requests.
-merge_request: 7746
-author: Jarka Kadlecova
diff --git a/changelogs/unreleased/24923_nested_tasks.yml b/changelogs/unreleased/24923_nested_tasks.yml
new file mode 100644
index 00000000000..de35cad3dd6
--- /dev/null
+++ b/changelogs/unreleased/24923_nested_tasks.yml
@@ -0,0 +1,4 @@
+---
+title: Fix nested tasks in ordered list
+merge_request: 8626
+author:
diff --git a/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml b/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml
deleted file mode 100644
index 0c9853de3b6..00000000000
--- a/changelogs/unreleased/25277-milestone-counter-number-with-delimiter.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added number_with_delimiter to counter on milestone panels
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml b/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml
deleted file mode 100644
index 13d3476fe39..00000000000
--- a/changelogs/unreleased/25371-environments-date-created-column-is-not-labeled.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds label to Environments "Date Created"
-merge_request: 8376
-author: Saad Shahd
diff --git a/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml b/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml
deleted file mode 100644
index e60c42cdfba..00000000000
--- a/changelogs/unreleased/25430-make-colors-in-runner-status-more-colorblind-friendly.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change status colors of runners to better defaults
-merge_request:
-author:
diff --git a/changelogs/unreleased/25580-trucate-dropdown-for-long-branch.yml b/changelogs/unreleased/25580-trucate-dropdown-for-long-branch.yml
deleted file mode 100644
index 18d3ac050ae..00000000000
--- a/changelogs/unreleased/25580-trucate-dropdown-for-long-branch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resolves overflow in compare branch and tags dropdown
-merge_request: 8118
-author:
diff --git a/changelogs/unreleased/25678-remove-user-build.yml b/changelogs/unreleased/25678-remove-user-build.yml
deleted file mode 100644
index 873e637d670..00000000000
--- a/changelogs/unreleased/25678-remove-user-build.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: remove build_user
-merge_request: 8162
-author: Arsenev Vladislav
diff --git a/changelogs/unreleased/25701-standardize-text-colors.yml b/changelogs/unreleased/25701-standardize-text-colors.yml
deleted file mode 100644
index a48ca6c187d..00000000000
--- a/changelogs/unreleased/25701-standardize-text-colors.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 25701 standardize text colors
-merge_request:
-author:
diff --git a/changelogs/unreleased/25705-your-commands-have-been-executed-is-overkill.yml b/changelogs/unreleased/25705-your-commands-have-been-executed-is-overkill.yml
deleted file mode 100644
index 850e98518a6..00000000000
--- a/changelogs/unreleased/25705-your-commands-have-been-executed-is-overkill.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: Replace wording for slash command confirmation message
-merge_request: 8123
diff --git a/changelogs/unreleased/25725-remove-window-object.yml b/changelogs/unreleased/25725-remove-window-object.yml
deleted file mode 100644
index c64b71ddd33..00000000000
--- a/changelogs/unreleased/25725-remove-window-object.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Removes unneeded `window` declaration in environments related code
-merge_request: 8456
-author:
diff --git a/changelogs/unreleased/25776-alerts-should-be-responsive.yml b/changelogs/unreleased/25776-alerts-should-be-responsive.yml
deleted file mode 100644
index 15006523d3e..00000000000
--- a/changelogs/unreleased/25776-alerts-should-be-responsive.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Changed alerts to be responsive, centered text on smaller viewports
-merge_request: 8424
-author: Connor Smallman
diff --git a/changelogs/unreleased/25829-update-username-button-remains-disabled-upon-failure.yml b/changelogs/unreleased/25829-update-username-button-remains-disabled-upon-failure.yml
deleted file mode 100644
index c82bacd8bcd..00000000000
--- a/changelogs/unreleased/25829-update-username-button-remains-disabled-upon-failure.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: re-enable change username button after failure
-merge_request: 8332
-author:
diff --git a/changelogs/unreleased/25898-ci-icon-color-mr.yml b/changelogs/unreleased/25898-ci-icon-color-mr.yml
deleted file mode 100644
index dd0f93e176f..00000000000
--- a/changelogs/unreleased/25898-ci-icon-color-mr.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Adds CSS class to status icon on MR widget to prevent non-colored icon
-merge_request: 8219
-author:
diff --git a/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml b/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml
deleted file mode 100644
index c28cf7a0f86..00000000000
--- a/changelogs/unreleased/25941-odd-overflow-behavior-for-long-issue-headers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change earlier to task_status_short to avoid titlebar line wraps
-merge_request:
-author:
diff --git a/changelogs/unreleased/25946-manual-pipeline-dropdown-casing.yml b/changelogs/unreleased/25946-manual-pipeline-dropdown-casing.yml
deleted file mode 100644
index b753c823348..00000000000
--- a/changelogs/unreleased/25946-manual-pipeline-dropdown-casing.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use original casing for build action text
-merge_request: 8387
-author:
diff --git a/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml b/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml
deleted file mode 100644
index 206be8fe3cb..00000000000
--- a/changelogs/unreleased/25985-combine-members-and-groups-settings-pages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Combined the settings options project members and groups into a single one
- called members
-merge_request:
-author:
diff --git a/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
new file mode 100644
index 00000000000..e67a9c0da15
--- /dev/null
+++ b/changelogs/unreleased/25989-fix-rogue-scrollbars-on-comments.yml
@@ -0,0 +1,4 @@
+---
+title: Remove rogue scrollbars for issue comments with inline elements
+merge_request:
+author:
diff --git a/changelogs/unreleased/25996-Move-award-emoji-out-of-the-discussion-tab-for-MR.yml b/changelogs/unreleased/25996-Move-award-emoji-out-of-the-discussion-tab-for-MR.yml
deleted file mode 100644
index e05e2dd6fed..00000000000
--- a/changelogs/unreleased/25996-Move-award-emoji-out-of-the-discussion-tab-for-MR.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move award emoji's out of the discussion tab for merge requests
-merge_request:
-author:
diff --git a/changelogs/unreleased/26014-fix-update-doc.yml b/changelogs/unreleased/26014-fix-update-doc.yml
deleted file mode 100644
index 419c032cb0f..00000000000
--- a/changelogs/unreleased/26014-fix-update-doc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Re-order update steps in the 8.14 -> 8.15 upgrade guide
-merge_request:
-author:
diff --git a/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml b/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml
deleted file mode 100644
index 85440eb86f9..00000000000
--- a/changelogs/unreleased/26051-fix-missing-endpoint-route-method.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't instrument 405 Grape calls
-merge_request: 8445
-author:
diff --git a/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml b/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml
deleted file mode 100644
index cde0d114d7c..00000000000
--- a/changelogs/unreleased/26109-preserve-scroll-position-on-autoreload.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Scroll to bottom on build completion if autoscroll was active
-merge_request: 8391
-author:
diff --git a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
new file mode 100644
index 00000000000..b2f5294d380
--- /dev/null
+++ b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml
@@ -0,0 +1,4 @@
+---
+title: Add sorting pipeline for a commit
+merge_request: 8319
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/26129-add-link-to-branches-page.yml b/changelogs/unreleased/26129-add-link-to-branches-page.yml
deleted file mode 100644
index aceb92dbb9c..00000000000
--- a/changelogs/unreleased/26129-add-link-to-branches-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Convert project setting text into protected branch path link
-merge_request: 8377
-author: Ken Ding
diff --git a/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml b/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml
deleted file mode 100644
index 40183f8d3fa..00000000000
--- a/changelogs/unreleased/26134-ctrl-enter-does-not-submit-a-new-merge-request.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make CTRL+Enter submits a new merge request
-merge_request: 8360
-author: Saad Shahd
diff --git a/changelogs/unreleased/26155-merge-request-tabs-don-t-render-when-no-commits-available.yml b/changelogs/unreleased/26155-merge-request-tabs-don-t-render-when-no-commits-available.yml
deleted file mode 100644
index 242a77b5d48..00000000000
--- a/changelogs/unreleased/26155-merge-request-tabs-don-t-render-when-no-commits-available.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: display merge request discussion tab for empty branches
-merge_request: 8347
-author:
diff --git a/changelogs/unreleased/26192-fixes-too-short-input.yml b/changelogs/unreleased/26192-fixes-too-short-input.yml
deleted file mode 100644
index ff707f4694d..00000000000
--- a/changelogs/unreleased/26192-fixes-too-short-input.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes too short input for placeholder message in commit listing page
-merge_request: 8367
-author:
diff --git a/changelogs/unreleased/26207-add-hover-animations.yml b/changelogs/unreleased/26207-add-hover-animations.yml
deleted file mode 100644
index 12a69d04717..00000000000
--- a/changelogs/unreleased/26207-add-hover-animations.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add various hover animations throughout the application
-merge_request:
-author:
diff --git a/changelogs/unreleased/26226-generate-all-haml-fixtures-within-teaspoon-fixtures-task.yml b/changelogs/unreleased/26226-generate-all-haml-fixtures-within-teaspoon-fixtures-task.yml
deleted file mode 100644
index 28981291132..00000000000
--- a/changelogs/unreleased/26226-generate-all-haml-fixtures-within-teaspoon-fixtures-task.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Precompile all JavaScript fixtures
-merge_request: 8384
-author:
diff --git a/changelogs/unreleased/26238-buttons-not-accessible.yml b/changelogs/unreleased/26238-buttons-not-accessible.yml
deleted file mode 100644
index 34d38d45709..00000000000
--- a/changelogs/unreleased/26238-buttons-not-accessible.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes buttons not being accessible via the keyboard when creating new group
-merge_request: 8469
-author:
diff --git a/changelogs/unreleased/26261-post-api-v3-projects-idorproject-commits-commits-does-not-work-with-project-path.yml b/changelogs/unreleased/26261-post-api-v3-projects-idorproject-commits-commits-does-not-work-with-project-path.yml
deleted file mode 100644
index 37bd7e46b49..00000000000
--- a/changelogs/unreleased/26261-post-api-v3-projects-idorproject-commits-commits-does-not-work-with-project-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Commits API to accept a Project path upon POST
-merge_request:
-author:
diff --git a/changelogs/unreleased/26352-user-dropdown-settings.yml b/changelogs/unreleased/26352-user-dropdown-settings.yml
deleted file mode 100644
index 19bd47b8673..00000000000
--- a/changelogs/unreleased/26352-user-dropdown-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 26352 Change Profile settings to User / Settings
-merge_request:
-author:
diff --git a/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml b/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml
deleted file mode 100644
index 43afdf45013..00000000000
--- a/changelogs/unreleased/26435-show-project-avatars-on-mobile.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display project avatars on Admin Area and Projects pages for mobile views
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
new file mode 100644
index 00000000000..fb5274e5253
--- /dev/null
+++ b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Improve button accessibility on pipelines page
+merge_request: 8561
+author:
diff --git a/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml b/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml
deleted file mode 100644
index b4aef8fe3da..00000000000
--- a/changelogs/unreleased/26445-make-icon-buttons-accessible-via-keyboard.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make play button on Pipelines page accessible via keyboard
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml b/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml
deleted file mode 100644
index 83f6233dd88..00000000000
--- a/changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Made download artifacts button accessible via keyboard by changing it from
- an anchor tag to an actual button
-merge_request:
-author: Ryan Harris
diff --git a/changelogs/unreleased/26447-fix-tab-list-order.yml b/changelogs/unreleased/26447-fix-tab-list-order.yml
new file mode 100644
index 00000000000..351c53bd076
--- /dev/null
+++ b/changelogs/unreleased/26447-fix-tab-list-order.yml
@@ -0,0 +1,4 @@
+---
+title: Fix tab index order on branch commits list page
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
new file mode 100644
index 00000000000..87ae8233c4a
--- /dev/null
+++ b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Sort by Recent Sign-in in Admin Area
+merge_request: 8637
+author: Poornima M
diff --git a/changelogs/unreleased/26504-mr-discussion-btn.yml b/changelogs/unreleased/26504-mr-discussion-btn.yml
deleted file mode 100644
index dec74ec61b1..00000000000
--- a/changelogs/unreleased/26504-mr-discussion-btn.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 26504 Fix styling of MR jump to discussion button
-merge_request:
-author:
diff --git a/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml b/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml
deleted file mode 100644
index 5891a5ef6e8..00000000000
--- a/changelogs/unreleased/26587-metrics-middleware-endpoint-is-nil.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Check for env[Grape::Env::GRAPE_ROUTING_ARGS] instead of endpoint.route
-merge_request: 8544
-author:
diff --git a/changelogs/unreleased/26615-pipeline-status-cell.yml b/changelogs/unreleased/26615-pipeline-status-cell.yml
deleted file mode 100644
index 9a19b041e63..00000000000
--- a/changelogs/unreleased/26615-pipeline-status-cell.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes pipeline status cell is too wide by adding missing classes in table head cells
-merge_request: 8549
-author:
diff --git a/changelogs/unreleased/26616-fix-search-group-project-filters.yml b/changelogs/unreleased/26616-fix-search-group-project-filters.yml
deleted file mode 100644
index 0fd0dbbfc24..00000000000
--- a/changelogs/unreleased/26616-fix-search-group-project-filters.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix search group/project filtering to show results
-merge_request:
-author:
diff --git a/changelogs/unreleased/26785-fix-droplab-in-ie-11-v1.yml b/changelogs/unreleased/26785-fix-droplab-in-ie-11-v1.yml
new file mode 100644
index 00000000000..76e9b19b828
--- /dev/null
+++ b/changelogs/unreleased/26785-fix-droplab-in-ie-11-v1.yml
@@ -0,0 +1,4 @@
+---
+title: Add some basic fixes for IE11/Edge
+merge_request:
+author:
diff --git a/changelogs/unreleased/26787-add-copy-icon-hover-state.yml b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
new file mode 100644
index 00000000000..31f1812c6f8
--- /dev/null
+++ b/changelogs/unreleased/26787-add-copy-icon-hover-state.yml
@@ -0,0 +1,4 @@
+---
+title: Add hover style to copy icon on commit page header
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/27066-textarea-border.yml b/changelogs/unreleased/27066-textarea-border.yml
new file mode 100644
index 00000000000..e45cb3aced5
--- /dev/null
+++ b/changelogs/unreleased/27066-textarea-border.yml
@@ -0,0 +1,4 @@
+---
+title: Remove blue border from comment box hover
+merge_request:
+author:
diff --git a/changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.yml b/changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.yml
deleted file mode 100644
index 74412c32375..00000000000
--- a/changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes issue boards list colored top border visual glitch
-merge_request: 7898
-author: Pier Paolo Ramon
diff --git a/changelogs/unreleased/8-15-stable.yml b/changelogs/unreleased/8-15-stable.yml
new file mode 100644
index 00000000000..75502e139e7
--- /dev/null
+++ b/changelogs/unreleased/8-15-stable.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure export files are removed after a namespace is deleted
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml b/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml
deleted file mode 100644
index 4d83d744be7..00000000000
--- a/changelogs/unreleased/add-changelog-search-bar-first-iteration.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Search bar redesign first iteration
-merge_request: 7345
-author:
diff --git a/changelogs/unreleased/add_email_password_confirmation.yml b/changelogs/unreleased/add_email_password_confirmation.yml
deleted file mode 100644
index 92f9b9b7a6d..00000000000
--- a/changelogs/unreleased/add_email_password_confirmation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add email confirmation field to registration form
-merge_request: 7432
-author:
diff --git a/changelogs/unreleased/additional-award-emoji-repositioning-fixes.yml b/changelogs/unreleased/additional-award-emoji-repositioning-fixes.yml
deleted file mode 100644
index 8d5a94c3aa8..00000000000
--- a/changelogs/unreleased/additional-award-emoji-repositioning-fixes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Removed bottom padding from merge manually from CLI because of repositioning award emoji's
-merge_request:
-author:
diff --git a/changelogs/unreleased/allow_plus_sign_for_snippets.yml b/changelogs/unreleased/allow_plus_sign_for_snippets.yml
deleted file mode 100644
index 62d9dd74d07..00000000000
--- a/changelogs/unreleased/allow_plus_sign_for_snippets.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow to use + symbol in filenames
-merge_request: 6644
-author: blackst0ne
diff --git a/changelogs/unreleased/asciidoctor-plantuml.yml b/changelogs/unreleased/asciidoctor-plantuml.yml
deleted file mode 100644
index ba6ef7c0800..00000000000
--- a/changelogs/unreleased/asciidoctor-plantuml.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add support for PlantUML diagrams in AsciiDoc documents.
-merge_request: 7810
-author: Horacio Sanson
diff --git a/changelogs/unreleased/badge-color-on-white-bg.yml b/changelogs/unreleased/badge-color-on-white-bg.yml
deleted file mode 100644
index 680d7ff11f0..00000000000
--- a/changelogs/unreleased/badge-color-on-white-bg.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added lighter count badge background-color for on white backgrounds
-merge_request: 7873
-author:
diff --git a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
new file mode 100644
index 00000000000..77750b55e7e
--- /dev/null
+++ b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
@@ -0,0 +1,4 @@
+---
+title: Hide version check image if there is no internet connection
+merge_request: 8355
+author: Ken Ding
diff --git a/changelogs/unreleased/bug-project-feature-compatibility.yml b/changelogs/unreleased/bug-project-feature-compatibility.yml
deleted file mode 100644
index 2124ee085e0..00000000000
--- a/changelogs/unreleased/bug-project-feature-compatibility.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Mutate the attribute instead of issuing a write operation to the DB in `ProjectFeaturesCompatibility`
- concern.
-merge_request: 8552
-author:
diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml
new file mode 100644
index 00000000000..6aa4a5664e7
--- /dev/null
+++ b/changelogs/unreleased/clipboard-button-commit-sha.yml
@@ -0,0 +1,3 @@
+---
+title: 'Copy commit SHA to clipboard'
+merge_request: 8547
diff --git a/changelogs/unreleased/clipboard-button-text.yml b/changelogs/unreleased/clipboard-button-text.yml
deleted file mode 100644
index dc93da60426..00000000000
--- a/changelogs/unreleased/clipboard-button-text.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: 'Copy <some text> to clipboard'
-merge_request: 8535
diff --git a/changelogs/unreleased/didemacet-ci-lint-page.yml b/changelogs/unreleased/didemacet-ci-lint-page.yml
deleted file mode 100644
index 07386321c9d..00000000000
--- a/changelogs/unreleased/didemacet-ci-lint-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change CI template linter textarea with Ace Editor
-merge_request: 8452
-author: Didem Acet
diff --git a/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
new file mode 100644
index 00000000000..6dd0d748001
--- /dev/null
+++ b/changelogs/unreleased/disable-autologin-on-email-confirmation-links.yml
@@ -0,0 +1,4 @@
+---
+title: Disable automatic login after clicking email confirmation links
+merge_request: 7472
+author:
diff --git a/changelogs/unreleased/dot-in-project-queries.yml b/changelogs/unreleased/dot-in-project-queries.yml
deleted file mode 100644
index fc48dc7b74d..00000000000
--- a/changelogs/unreleased/dot-in-project-queries.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow API query to find projects with dots in their name
-merge_request:
-author: Bruno Melli
diff --git a/changelogs/unreleased/dz-nested-group-misc.yml b/changelogs/unreleased/dz-nested-group-misc.yml
deleted file mode 100644
index 9c9d0b1c644..00000000000
--- a/changelogs/unreleased/dz-nested-group-misc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show nested groups tab on group page
-merge_request: 8308
-author:
diff --git a/changelogs/unreleased/dz-rename-invalid-users.yml b/changelogs/unreleased/dz-rename-invalid-users.yml
deleted file mode 100644
index f420b069531..00000000000
--- a/changelogs/unreleased/dz-rename-invalid-users.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename users with namespace ending with .git
-merge_request: 8309
-author:
diff --git a/changelogs/unreleased/env-var-in-redis-config.yml b/changelogs/unreleased/env-var-in-redis-config.yml
deleted file mode 100644
index 561ea7f514e..00000000000
--- a/changelogs/unreleased/env-var-in-redis-config.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow to use ENV variables in redis config
-merge_request: 8073
-author: Semyon Pupkov
diff --git a/changelogs/unreleased/feature-1376-allow-write-access-deploy-keys.yml b/changelogs/unreleased/feature-1376-allow-write-access-deploy-keys.yml
deleted file mode 100644
index 0fd590a877b..00000000000
--- a/changelogs/unreleased/feature-1376-allow-write-access-deploy-keys.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow to add deploy keys with write-access
-merge_request: 5807
-author: Ali Ibrahim
diff --git a/changelogs/unreleased/feature-admin-merge-groups-and-projects.yml b/changelogs/unreleased/feature-admin-merge-groups-and-projects.yml
deleted file mode 100644
index 0c0b74b686a..00000000000
--- a/changelogs/unreleased/feature-admin-merge-groups-and-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Merged the 'Groups' and 'Projects' tabs when viewing user profiles
-merge_request: 8323
-author: James Gregory
diff --git a/changelogs/unreleased/feature-log-ldap-to-application-log.yml b/changelogs/unreleased/feature-log-ldap-to-application-log.yml
deleted file mode 100644
index 4cfbc23edb7..00000000000
--- a/changelogs/unreleased/feature-log-ldap-to-application-log.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Log LDAP blocking/unblocking events to application log
-merge_request: 8042
-author: Markus Koller
diff --git a/changelogs/unreleased/feature-more-storage-statistics.yml b/changelogs/unreleased/feature-more-storage-statistics.yml
deleted file mode 100644
index 824fd36dc34..00000000000
--- a/changelogs/unreleased/feature-more-storage-statistics.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add more storage statistics
-merge_request: 7754
-author: Markus Koller
diff --git a/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
new file mode 100644
index 00000000000..5fba0332881
--- /dev/null
+++ b/changelogs/unreleased/feature-success-warning-icons-in-stages-builds.yml
@@ -0,0 +1,4 @@
+---
+title: Use warning icon in mini-graph if stage passed conditionally
+merge_request: 8503
+author:
diff --git a/changelogs/unreleased/filename-to-file-path.yml b/changelogs/unreleased/filename-to-file-path.yml
deleted file mode 100644
index 3c6c838595a..00000000000
--- a/changelogs/unreleased/filename-to-file-path.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: Rename filename to file path in tooltip of file header in merge request diff
-merge_request: 8314 \ No newline at end of file
diff --git a/changelogs/unreleased/fill-authorized-projects.yml b/changelogs/unreleased/fill-authorized-projects.yml
deleted file mode 100644
index e8e33011a15..00000000000
--- a/changelogs/unreleased/fill-authorized-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fill missing authorized projects rows
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-api-deprecation.yml b/changelogs/unreleased/fix-api-deprecation.yml
deleted file mode 100644
index 90285ddf058..00000000000
--- a/changelogs/unreleased/fix-api-deprecation.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix a Grape deprecation, use `#request_method` instead of `#route_method`
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-api-mr-permissions.yml b/changelogs/unreleased/fix-api-mr-permissions.yml
new file mode 100644
index 00000000000..33b677b1f29
--- /dev/null
+++ b/changelogs/unreleased/fix-api-mr-permissions.yml
@@ -0,0 +1,4 @@
+---
+title: Don't allow project guests to subscribe to merge requests through the API
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/fix-blame-500.yml b/changelogs/unreleased/fix-blame-500.yml
deleted file mode 100644
index 379d81aaa44..00000000000
--- a/changelogs/unreleased/fix-blame-500.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix blame 500 error on invalid path.
-merge_request: 25761
-author: Jeff Stubler
diff --git a/changelogs/unreleased/fix-boards-search-typo.yml b/changelogs/unreleased/fix-boards-search-typo.yml
deleted file mode 100644
index 0c083fc0d10..00000000000
--- a/changelogs/unreleased/fix-boards-search-typo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'Fix typo: seach to search'
-merge_request: 8370
-author:
diff --git a/changelogs/unreleased/fix-broken-url-on-group-avatar.yml b/changelogs/unreleased/fix-broken-url-on-group-avatar.yml
deleted file mode 100644
index 7ce22b4826e..00000000000
--- a/changelogs/unreleased/fix-broken-url-on-group-avatar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix broken url on group avatar
-merge_request: 8464
-author: hogewest
diff --git a/changelogs/unreleased/fix-build-sort-order.yml b/changelogs/unreleased/fix-build-sort-order.yml
deleted file mode 100644
index a6d6371f69a..00000000000
--- a/changelogs/unreleased/fix-build-sort-order.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Sort numbers in build names more intelligently
-merge_request: 8277
-author:
diff --git a/changelogs/unreleased/fix-copy-issues-empty-state.yml b/changelogs/unreleased/fix-copy-issues-empty-state.yml
deleted file mode 100644
index a87b7612217..00000000000
--- a/changelogs/unreleased/fix-copy-issues-empty-state.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve copy in Issue Tracker empty state
-merge_request: 8202
-author:
diff --git a/changelogs/unreleased/fix-guest-access-posting-to-notes.yml b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
new file mode 100644
index 00000000000..81377c0c6f0
--- /dev/null
+++ b/changelogs/unreleased/fix-guest-access-posting-to-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent users from creating notes on resources they can't access
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml b/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml
deleted file mode 100644
index 3d8cf1c74a2..00000000000
--- a/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide build artifacts keep button if operation is not allowed
-merge_request: 8501
-author:
diff --git a/changelogs/unreleased/fix-light-hr-in-descriptions.yml b/changelogs/unreleased/fix-light-hr-in-descriptions.yml
deleted file mode 100644
index 8efd471e416..00000000000
--- a/changelogs/unreleased/fix-light-hr-in-descriptions.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Darkened hr border color in descriptions because of update of bootstrap
-merge_request: 8333
-author:
diff --git a/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml b/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml
deleted file mode 100644
index bc2068b8177..00000000000
--- a/changelogs/unreleased/fix-more-orphans-remove-undeleted-groups.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove extra orphaned rows when removing stray namespaces
-merge_request: 7841
-author:
diff --git a/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml b/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml
deleted file mode 100644
index 98066537723..00000000000
--- a/changelogs/unreleased/fix-no-milestone-option-for-projects-endpoint-23194.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: 'API: fix query response for `/projects/:id/issues?milestone="No%20Milestone"`'
-merge_request: 8457
-author: Panagiotis Atmatzidis, David Eisner
diff --git a/changelogs/unreleased/fix-project-delete-tooltip.yml b/changelogs/unreleased/fix-project-delete-tooltip.yml
deleted file mode 100644
index 42fd9c32519..00000000000
--- a/changelogs/unreleased/fix-project-delete-tooltip.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix project queued for deletion re-creation tooltip
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-serialized-commit-path.yml b/changelogs/unreleased/fix-serialized-commit-path.yml
deleted file mode 100644
index 4e4df503874..00000000000
--- a/changelogs/unreleased/fix-serialized-commit-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix links to commits pages on pipelines list page
-merge_request: 8558
-author:
diff --git a/changelogs/unreleased/fix-timezone-due-date-picker.yml b/changelogs/unreleased/fix-timezone-due-date-picker.yml
deleted file mode 100644
index 2e6b71c70ca..00000000000
--- a/changelogs/unreleased/fix-timezone-due-date-picker.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix date inconsistency on due date picker
-merge_request: 7422
-author: Giuliano Varriale
diff --git a/changelogs/unreleased/fix-user-api-confirm-param.yml b/changelogs/unreleased/fix-user-api-confirm-param.yml
deleted file mode 100644
index 42642576634..00000000000
--- a/changelogs/unreleased/fix-user-api-confirm-param.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix 500 error when POSTing to Users API with optional confirm param
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
new file mode 100644
index 00000000000..c9edd1de86c
--- /dev/null
+++ b/changelogs/unreleased/fix-users-deleting-public-deployment-keys.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent users from deleting system deploy keys via the project deploy key API
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml
new file mode 100644
index 00000000000..4551212759f
--- /dev/null
+++ b/changelogs/unreleased/fix_broken_diff_discussions.yml
@@ -0,0 +1,4 @@
+---
+title: Make MR-review-discussions more reliable
+merge_request:
+author:
diff --git a/changelogs/unreleased/get_last_used_date_of_ssh_key.yml b/changelogs/unreleased/get_last_used_date_of_ssh_key.yml
deleted file mode 100644
index b753949922c..00000000000
--- a/changelogs/unreleased/get_last_used_date_of_ssh_key.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Record and show last used date of SSH Keys
-merge_request: 8113
-author: Vincent Wong
diff --git a/changelogs/unreleased/issue-boards-animate.yml b/changelogs/unreleased/issue-boards-animate.yml
deleted file mode 100644
index 28394aec3d5..00000000000
--- a/changelogs/unreleased/issue-boards-animate.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added animations to issue boards interactions
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue-filter-click-to-search.yml b/changelogs/unreleased/issue-filter-click-to-search.yml
new file mode 100644
index 00000000000..c024ea48dc7
--- /dev/null
+++ b/changelogs/unreleased/issue-filter-click-to-search.yml
@@ -0,0 +1,4 @@
+---
+title: allow issue filter bar to be operated with mouse only
+merge_request: 8681
+author:
diff --git a/changelogs/unreleased/issue_22664.yml b/changelogs/unreleased/issue_22664.yml
deleted file mode 100644
index 18a8d9ec6be..00000000000
--- a/changelogs/unreleased/issue_22664.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Check if user can read project before being assigned to issue
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_25017.yml b/changelogs/unreleased/issue_25017.yml
deleted file mode 100644
index 09126ae81bc..00000000000
--- a/changelogs/unreleased/issue_25017.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show 'too many changes' message for created merge requests when they are too large
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_25578.yml b/changelogs/unreleased/issue_25578.yml
deleted file mode 100644
index e10f1d232af..00000000000
--- a/changelogs/unreleased/issue_25578.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix redirect after update file when user has forked project
-merge_request:
-author:
diff --git a/changelogs/unreleased/issue_25682.yml b/changelogs/unreleased/issue_25682.yml
deleted file mode 100644
index a50138756ba..00000000000
--- a/changelogs/unreleased/issue_25682.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Parse JIRA issue references even if Issue Tracker is disabled
-merge_request:
-author:
diff --git a/changelogs/unreleased/issues-8081.yml b/changelogs/unreleased/issues-8081.yml
deleted file mode 100644
index 82f746937bc..00000000000
--- a/changelogs/unreleased/issues-8081.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: change 'gray' color theme name to 'black' to match the actual color
-merge_request: 7908
-author: BM5k
diff --git a/changelogs/unreleased/ldap_maint_task.yml b/changelogs/unreleased/ldap_maint_task.yml
deleted file mode 100644
index 8acffba0ce5..00000000000
--- a/changelogs/unreleased/ldap_maint_task.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add LDAP Rake task to rename a provider
-merge_request: 2181
-author:
diff --git a/changelogs/unreleased/login-page-font-size.yml b/changelogs/unreleased/login-page-font-size.yml
deleted file mode 100644
index e7775006673..00000000000
--- a/changelogs/unreleased/login-page-font-size.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Decreases font-size on login page
-merge_request:
-author:
diff --git a/changelogs/unreleased/merge-dropdown-this-context.yml b/changelogs/unreleased/merge-dropdown-this-context.yml
new file mode 100644
index 00000000000..5c4890fcaa2
--- /dev/null
+++ b/changelogs/unreleased/merge-dropdown-this-context.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed bug where links in merge dropdown wouldn't work
+merge_request:
+author:
diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
new file mode 100644
index 00000000000..f32b3aea3c8
--- /dev/null
+++ b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
@@ -0,0 +1,4 @@
+---
+title: adds avatar for discussion note
+merge_request: 8734
+author:
diff --git a/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml b/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml
deleted file mode 100644
index b8c7b78cf0d..00000000000
--- a/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed merge request tabs dont move when opening collapsed sidebar
-merge_request:
-author:
diff --git a/changelogs/unreleased/newline-eslint-rule.yml b/changelogs/unreleased/newline-eslint-rule.yml
new file mode 100644
index 00000000000..5ce080b6912
--- /dev/null
+++ b/changelogs/unreleased/newline-eslint-rule.yml
@@ -0,0 +1,4 @@
+---
+title: Flag multiple empty lines in eslint, fix offenses.
+merge_request: 8137
+author:
diff --git a/changelogs/unreleased/no_project_notes.yml b/changelogs/unreleased/no_project_notes.yml
new file mode 100644
index 00000000000..6106c027360
--- /dev/null
+++ b/changelogs/unreleased/no_project_notes.yml
@@ -0,0 +1,4 @@
+---
+title: Support notes when a project is not specified (personal snippet notes)
+merge_request: 8468
+author:
diff --git a/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml b/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml
deleted file mode 100644
index fd173031107..00000000000
--- a/changelogs/unreleased/nuke-ugly-spaces-in-changelog-generator.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove trailing whitespace when generating changelog entry
-merge_request: 7948
-author:
diff --git a/changelogs/unreleased/pc-add-gitaly-to-architecture.yml b/changelogs/unreleased/pc-add-gitaly-to-architecture.yml
deleted file mode 100644
index 7c18da698df..00000000000
--- a/changelogs/unreleased/pc-add-gitaly-to-architecture.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add Gitaly to the architecture documentation
-merge_request: 8264
-author: Pablo Carranza <pablo@gitlab.com>
diff --git a/changelogs/unreleased/pipelines-graph-html-css.yml b/changelogs/unreleased/pipelines-graph-html-css.yml
deleted file mode 100644
index ff0c3122fdb..00000000000
--- a/changelogs/unreleased/pipelines-graph-html-css.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown
-merge_request: 8443
-author:
diff --git a/changelogs/unreleased/pmq20-gitlab-ce-psvr-head-cache.yml b/changelogs/unreleased/pmq20-gitlab-ce-psvr-head-cache.yml
deleted file mode 100644
index 23230128dc9..00000000000
--- a/changelogs/unreleased/pmq20-gitlab-ce-psvr-head-cache.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Expire related caches after changing HEAD
-merge_request:
-author: Minqi Pan
diff --git a/changelogs/unreleased/re-style-issue-new-branch.yml b/changelogs/unreleased/re-style-issue-new-branch.yml
deleted file mode 100644
index 977a54ff2ae..00000000000
--- a/changelogs/unreleased/re-style-issue-new-branch.yml
+++ /dev/null
@@ -1,3 +0,0 @@
----
-title: Remove checking branches state in issue new branch button
-merge_request: 8023
diff --git a/changelogs/unreleased/recaptcha_500.yml b/changelogs/unreleased/recaptcha_500.yml
deleted file mode 100644
index de9ef183d5e..00000000000
--- a/changelogs/unreleased/recaptcha_500.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Properly handle failed reCAPTCHA on user registration
-merge_request: 8403
-author:
diff --git a/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
new file mode 100644
index 00000000000..e0f7e11b6d1
--- /dev/null
+++ b/changelogs/unreleased/redirect-to-commit-when-only-commit-found.yml
@@ -0,0 +1,5 @@
+---
+title: 'Search feature: redirects to commit page if query is commit sha and only commit
+ found'
+merge_request: 8028
+author: YarNayar
diff --git a/changelogs/unreleased/reduce-queries-milestone-index.yml b/changelogs/unreleased/reduce-queries-milestone-index.yml
deleted file mode 100644
index a779b58c973..00000000000
--- a/changelogs/unreleased/reduce-queries-milestone-index.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use cached values to compute total issues count in milestone index pages
-merge_request: 8518
-author:
diff --git a/changelogs/unreleased/refresh-authorizations-tighter-lease.yml b/changelogs/unreleased/refresh-authorizations-tighter-lease.yml
deleted file mode 100644
index ab42b2eb72d..00000000000
--- a/changelogs/unreleased/refresh-authorizations-tighter-lease.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Synchronize all project authorization refreshing work to prevent race conditions
-merge_request:
-author:
diff --git a/changelogs/unreleased/remove-project-authorizations-id-column.yml b/changelogs/unreleased/remove-project-authorizations-id-column.yml
deleted file mode 100644
index 24c86f0fb1b..00000000000
--- a/changelogs/unreleased/remove-project-authorizations-id-column.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove the project_authorizations.id column
-merge_request:
-author:
diff --git a/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml b/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml
deleted file mode 100644
index 47d484e5c84..00000000000
--- a/changelogs/unreleased/remove-successful-pipeline-emails-for-now.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make successful pipeline emails off for watchers
-merge_request: 8176
-author:
diff --git a/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml b/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml
deleted file mode 100644
index 8ec3cfdbb08..00000000000
--- a/changelogs/unreleased/restore-backup-when-env-variable-is-passed.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Restore backup correctly when "BACKUP" environment variable is passed
-merge_request: 8477
-author:
diff --git a/changelogs/unreleased/sandish-gitlab-ce-update_ret_val.yml b/changelogs/unreleased/sandish-gitlab-ce-update_ret_val.yml
deleted file mode 100644
index 7107ddfd982..00000000000
--- a/changelogs/unreleased/sandish-gitlab-ce-update_ret_val.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Ensure updating project settings shows a flash message on success
-merge_request: 8579
-author: Sandish Chen
diff --git a/changelogs/unreleased/single-edit-comment-widget-2.yml b/changelogs/unreleased/single-edit-comment-widget-2.yml
deleted file mode 100644
index e8b8beb15de..00000000000
--- a/changelogs/unreleased/single-edit-comment-widget-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactored note edit form to improve frontend performance on MR and Issues
- pages, especially pages with has a lot of discussions in it
-merge_request: 8356
-author:
diff --git a/changelogs/unreleased/support-google-cloud-storage-backups.yml b/changelogs/unreleased/support-google-cloud-storage-backups.yml
deleted file mode 100644
index cec279a5c73..00000000000
--- a/changelogs/unreleased/support-google-cloud-storage-backups.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Re-add Google Cloud Storage as a backup strategy
-merge_request:
-author:
diff --git a/changelogs/unreleased/switch-to-sassc.yml b/changelogs/unreleased/switch-to-sassc.yml
deleted file mode 100644
index 3e6c4baf6d9..00000000000
--- a/changelogs/unreleased/switch-to-sassc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Switch to sassc-rails for faster stylesheet compilation
-merge_request: 8556
-author: Richard Macklin
diff --git a/changelogs/unreleased/update-gitlab-markup-gem.yml b/changelogs/unreleased/update-gitlab-markup-gem.yml
deleted file mode 100644
index 96cdfd051f0..00000000000
--- a/changelogs/unreleased/update-gitlab-markup-gem.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update the gitlab-markup gem to the version 1.5.1
-merge_request: 8509
-author:
diff --git a/changelogs/unreleased/upgrade-omniauth.yml b/changelogs/unreleased/upgrade-omniauth.yml
new file mode 100644
index 00000000000..7e0334566dc
--- /dev/null
+++ b/changelogs/unreleased/upgrade-omniauth.yml
@@ -0,0 +1,4 @@
+---
+title: Upgrade omniauth gem to 1.3.2
+merge_request:
+author:
diff --git a/changelogs/unreleased/validate-title-length.yml b/changelogs/unreleased/validate-title-length.yml
deleted file mode 100644
index 7abf1c4d05a..00000000000
--- a/changelogs/unreleased/validate-title-length.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Validate label's title length"
-merge_request: 5767
-author: Tomáš Kukrál
diff --git a/changelogs/unreleased/view-ce-vs-ee.yml b/changelogs/unreleased/view-ce-vs-ee.yml
deleted file mode 100644
index 38bce4ac7c3..00000000000
--- a/changelogs/unreleased/view-ce-vs-ee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: About GitLab link in sidebar that links to help page
-merge_request: 8316
-author:
diff --git a/changelogs/unreleased/wip-mr-from-commits.yml b/changelogs/unreleased/wip-mr-from-commits.yml
new file mode 100644
index 00000000000..0083798be08
--- /dev/null
+++ b/changelogs/unreleased/wip-mr-from-commits.yml
@@ -0,0 +1,4 @@
+---
+title: Mark MR as WIP when pushing WIP commits
+merge_request: 8124
+author: Jurre Stender @jurre
diff --git a/changelogs/unreleased/zj-requeue-pending-delete.yml b/changelogs/unreleased/zj-requeue-pending-delete.yml
new file mode 100644
index 00000000000..464c5948f8c
--- /dev/null
+++ b/changelogs/unreleased/zj-requeue-pending-delete.yml
@@ -0,0 +1,4 @@
+---
+title: Requeue pending deletion projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-unadressable-url-variables.yml b/changelogs/unreleased/zj-unadressable-url-variables.yml
deleted file mode 100644
index 6c412bd0540..00000000000
--- a/changelogs/unreleased/zj-unadressable-url-variables.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't validate environment urls on .gitlab-ci.yml
-merge_request:
-author:
diff --git a/config/application.rb b/config/application.rb
index 8ce549cebf6..f00e58a36ca 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -88,6 +88,7 @@ module Gitlab
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
+ config.assets.precompile << "lib/vue_resource.js"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
@@ -98,6 +99,7 @@ module Gitlab
config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "merge_request_widget/ci_bundle.js"
+ config.assets.precompile << "issuable/issuable_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index ee97b4e42b9..906ec11f012 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -404,6 +404,12 @@ Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute
Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour
#
+# Gitaly
+#
+Settings['gitaly'] ||= Settingslogic.new({})
+Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH']
+
+#
# Testing settings
#
if Rails.env.test?
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 3b8771543e4..e0702e06cc9 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -1,3 +1,117 @@
+# Autoload all classes that we want to instrument, and instrument the methods we
+# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
+# that we can stub it for testing, as it is only called when metrics are
+# enabled.
+#
+# rubocop:disable Metrics/AbcSize
+def instrument_classes(instrumentation)
+ instrumentation.instrument_instance_methods(Gitlab::Shell)
+
+ instrumentation.instrument_methods(Gitlab::Git)
+
+ Gitlab::Git.constants.each do |name|
+ const = Gitlab::Git.const_get(name)
+
+ next unless const.is_a?(Module)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Path to search => prefix to strip from constant
+ paths_to_instrument = {
+ ['app', 'finders'] => ['app', 'finders'],
+ ['app', 'mailers', 'emails'] => ['app', 'mailers'],
+ ['app', 'services', '**'] => ['app', 'services'],
+ ['lib', 'gitlab', 'conflicts'] => ['lib'],
+ ['lib', 'gitlab', 'diff'] => ['lib'],
+ ['lib', 'gitlab', 'email', 'message'] => ['lib'],
+ ['lib', 'gitlab', 'checks'] => ['lib']
+ }
+
+ paths_to_instrument.each do |(path, prefix)|
+ prefix = Rails.root.join(*prefix)
+
+ Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
+ path = Pathname.new(file_path).relative_path_from(prefix)
+ const = path.to_s.sub('.rb', '').camelize.constantize
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
+ instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
+
+ [
+ :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
+ :Tag, :TagCollection, :Tree
+ ].each do |name|
+ const = Rugged.const_get(name)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Instruments all Banzai filters and reference parsers
+ {
+ Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
+ ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
+ }.each do |const_name, path|
+ Dir[path].each do |file|
+ klass = File.basename(file, File.extname(file)).camelize
+ const = Banzai.const_get(const_name).const_get(klass)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Banzai::Renderer)
+ instrumentation.instrument_methods(Banzai::Querying)
+
+ instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
+ instrumentation.instrument_instance_methods(Banzai::Redactor)
+ instrumentation.instrument_methods(Banzai::NoteRenderer)
+
+ [Issuable, Mentionable, Participable].each do |klass|
+ instrumentation.instrument_instance_methods(klass)
+ instrumentation.instrument_instance_methods(klass::ClassMethods)
+ end
+
+ instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
+ instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
+
+ # Instrument the classes used for checking if somebody has push access.
+ instrumentation.instrument_instance_methods(Gitlab::GitAccess)
+ instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
+
+ instrumentation.instrument_instance_methods(API::Helpers)
+
+ instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
+
+ instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
+ instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
+
+ [:XML, :HTML].each do |namespace|
+ namespace_mod = Nokogiri.const_get(namespace)
+
+ instrumentation.instrument_methods(namespace_mod)
+ instrumentation.instrument_methods(namespace_mod::Document)
+ end
+
+ instrumentation.instrument_methods(Rinku)
+ instrumentation.instrument_instance_methods(Repository)
+
+ instrumentation.instrument_methods(Gitlab::Highlight)
+ instrumentation.instrument_instance_methods(Gitlab::Highlight)
+
+ # This is a Rails scope so we have to instrument it manually.
+ instrumentation.instrument_method(Project, :visible_to_user)
+end
+# rubocop:enable Metrics/AbcSize
+
if Gitlab::Metrics.enabled?
require 'pathname'
require 'influxdb'
@@ -49,110 +163,7 @@ if Gitlab::Metrics.enabled?
end
Gitlab::Metrics::Instrumentation.configure do |config|
- config.instrument_instance_methods(Gitlab::Shell)
-
- config.instrument_methods(Gitlab::Git)
-
- Gitlab::Git.constants.each do |name|
- const = Gitlab::Git.const_get(name)
-
- next unless const.is_a?(Module)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Path to search => prefix to strip from constant
- paths_to_instrument = {
- ['app', 'finders'] => ['app', 'finders'],
- ['app', 'mailers', 'emails'] => ['app', 'mailers'],
- ['app', 'services', '**'] => ['app', 'services'],
- ['lib', 'gitlab', 'conflicts'] => ['lib'],
- ['lib', 'gitlab', 'diff'] => ['lib'],
- ['lib', 'gitlab', 'email', 'message'] => ['lib'],
- ['lib', 'gitlab', 'checks'] => ['lib']
- }
-
- paths_to_instrument.each do |(path, prefix)|
- prefix = Rails.root.join(*prefix)
-
- Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
- path = Pathname.new(file_path).relative_path_from(prefix)
- const = path.to_s.sub('.rb', '').camelize.constantize
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Premailer::Adapter::Nokogiri)
- config.instrument_instance_methods(Premailer::Adapter::Nokogiri)
-
- [
- :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
- :Tag, :TagCollection, :Tree
- ].each do |name|
- const = Rugged.const_get(name)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Instruments all Banzai filters and reference parsers
- {
- Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
- ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
- }.each do |const_name, path|
- Dir[path].each do |file|
- klass = File.basename(file, File.extname(file)).camelize
- const = Banzai.const_get(const_name).const_get(klass)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Banzai::Renderer)
- config.instrument_methods(Banzai::Querying)
-
- config.instrument_instance_methods(Banzai::ObjectRenderer)
- config.instrument_instance_methods(Banzai::Redactor)
- config.instrument_methods(Banzai::NoteRenderer)
-
- [Issuable, Mentionable, Participable].each do |klass|
- config.instrument_instance_methods(klass)
- config.instrument_instance_methods(klass::ClassMethods)
- end
-
- config.instrument_methods(Gitlab::ReferenceExtractor)
- config.instrument_instance_methods(Gitlab::ReferenceExtractor)
-
- # Instrument the classes used for checking if somebody has push access.
- config.instrument_instance_methods(Gitlab::GitAccess)
- config.instrument_instance_methods(Gitlab::GitAccessWiki)
-
- config.instrument_instance_methods(API::Helpers)
-
- config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
-
- config.instrument_instance_methods(Rouge::Plugins::Redcarpet)
- config.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
-
- [:XML, :HTML].each do |namespace|
- namespace_mod = Nokogiri.const_get(namespace)
-
- config.instrument_methods(namespace_mod)
- config.instrument_methods(namespace_mod::Document)
- end
-
- config.instrument_methods(Rinku)
- config.instrument_instance_methods(Repository)
-
- config.instrument_methods(Gitlab::Highlight)
- config.instrument_instance_methods(Gitlab::Highlight)
-
- # This is a Rails scope so we have to instrument it manually.
- config.instrument_method(Project, :visible_to_user)
+ instrument_classes(config)
end
GC::Profiler.enable
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 1fc6ed28c74..6620b765e02 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -307,9 +307,9 @@ constraints(ProjectUrlConstrainer.new) do
end
end
end
-
namespace :settings do
resource :members, only: [:show]
+ resource :integrations, only: [:show]
end
# Since both wiki and repository routing contains wildcard characters
diff --git a/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb b/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
new file mode 100644
index 00000000000..d5116dfab49
--- /dev/null
+++ b/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
@@ -0,0 +1,25 @@
+class AddEstimateToIssuablesCe < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ unless column_exists?(:issues, :time_estimate)
+ add_column :issues, :time_estimate, :integer
+ end
+
+ unless column_exists?(:merge_requests, :time_estimate)
+ add_column :merge_requests, :time_estimate, :integer
+ end
+ end
+
+ def down
+ if column_exists?(:issues, :time_estimate)
+ remove_column :issues, :time_estimate
+ end
+
+ if column_exists?(:merge_requests, :time_estimate)
+ remove_column :merge_requests, :time_estimate
+ end
+ end
+end
diff --git a/db/migrate/20161223034646_create_timelogs_ce.rb b/db/migrate/20161223034646_create_timelogs_ce.rb
new file mode 100644
index 00000000000..66d9cd823fb
--- /dev/null
+++ b/db/migrate/20161223034646_create_timelogs_ce.rb
@@ -0,0 +1,24 @@
+class CreateTimelogsCe < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ unless table_exists?(:timelogs)
+ create_table :timelogs do |t|
+ t.integer :time_spent, null: false
+ t.references :trackable, polymorphic: true
+ t.references :user
+
+ t.timestamps null: false
+ end
+
+ add_index :timelogs, [:trackable_type, :trackable_id]
+ add_index :timelogs, :user_id
+ end
+ end
+
+ def down
+ drop_table :timelogs if table_exists?(:timelogs)
+ end
+end
diff --git a/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb
new file mode 100644
index 00000000000..4ea953f2b78
--- /dev/null
+++ b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb
@@ -0,0 +1,17 @@
+class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, [:status, :type, :runner_id]
+ end
+
+ def down
+ if index_exists?(:ci_builds, [:status, :type, :runner_id])
+ remove_index :ci_builds, column: [:status, :type, :runner_id]
+ end
+ end
+end
diff --git a/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb
new file mode 100644
index 00000000000..620befcf4d7
--- /dev/null
+++ b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb
@@ -0,0 +1,17 @@
+class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_runners, :is_shared
+ end
+
+ def down
+ if index_exists?(:ci_runners, :is_shared)
+ remove_index :ci_runners, :is_shared
+ end
+ end
+end
diff --git a/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb
new file mode 100644
index 00000000000..f399950bd5e
--- /dev/null
+++ b/db/post_migrate/20170104150317_requeue_pending_delete_projects.rb
@@ -0,0 +1,49 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RequeuePendingDeleteProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ admin = User.find_by(admin: true)
+ return unless admin
+
+ @offset = 0
+
+ loop do
+ ids = pending_delete_batch
+
+ break if ids.rows.count.zero?
+
+ args = ids.map { |id| [id['id'], admin.id, {}] }
+
+ Sidekiq::Client.push_bulk('class' => "ProjectDestroyWorker", 'args' => args)
+
+ @offset += 1
+ end
+ end
+
+ def down
+ # noop
+ end
+
+ private
+
+ def pending_delete_batch
+ connection.exec_query(find_batch)
+ end
+
+ BATCH_SIZE = 5000
+
+ def find_batch
+ projects = Arel::Table.new(:projects)
+ projects.project(projects[:id]).
+ where(projects[:pending_delete].eq(true)).
+ where(projects[:namespace_id].not_eq(nil)).
+ skip(@offset * BATCH_SIZE).
+ take(BATCH_SIZE).
+ to_sql
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c58a886b0fa..3c836db27fc 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: 20170106172224) do
+ActiveRecord::Schema.define(version: 20170121130655) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -224,6 +224,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_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
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
@@ -327,6 +328,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.boolean "locked", default: false, null: false
end
+ 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", ["token"], name: "index_ci_runners_on_token", using: :btree
@@ -506,6 +508,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.integer "lock_version"
t.text "title_html"
t.text "description_html"
+ t.integer "time_estimate"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -685,6 +688,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.integer "lock_version"
t.text "title_html"
t.text "description_html"
+ t.integer "time_estimate"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -1128,6 +1132,18 @@ ActiveRecord::Schema.define(version: 20170106172224) do
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
+ create_table "timelogs", force: :cascade do |t|
+ t.integer "time_spent", null: false
+ t.integer "trackable_id"
+ t.string "trackable_type"
+ t.integer "user_id"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree
+ add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
+
create_table "todos", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
diff --git a/doc/README.md b/doc/README.md
index ee69684b53b..993b30ccdb5 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,8 +1,12 @@
# GitLab Community Edition documentation
+## University
+
+[University](university/README.md) contain guides to learn Git and GitLab through courses and videos.
+
## User documentation
-- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc.
+- [Account Security](user/profile/account/two_factor_authentication.md) Securing your account via two-factor authentication, etc.
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
@@ -19,7 +23,6 @@
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
-- [University](university/README.md) Learn Git and GitLab through videos and courses.
- [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations.
@@ -50,7 +53,8 @@
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [GitLab Performance Monitoring](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics.
- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
index 79904916b7e..8b106e89cc2 100644
--- a/doc/administration/monitoring/performance/introduction.md
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -12,6 +12,11 @@ documents in order to understand and properly configure GitLab Performance Monit
- [InfluxDB Schema](influxdb_schema.md)
- [Grafana Install/Configuration](grafana_configuration.md)
+>**Note:**
+Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect
+metrics. It will eventually replace InfluxDB when their metrics collection is
+on par. Read more in the [Prometheus documentation](prometheus.md).
+
## Introduction to GitLab Performance Monitoring
GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
diff --git a/doc/administration/monitoring/performance/prometheus.md b/doc/administration/monitoring/performance/prometheus.md
new file mode 100644
index 00000000000..51c63325064
--- /dev/null
+++ b/doc/administration/monitoring/performance/prometheus.md
@@ -0,0 +1,102 @@
+# GitLab Prometheus
+
+>**Notes:**
+- Prometheus and the node exporter are bundled in the Omnibus GitLab package
+ since GitLab 8.16. For installations from source you will have to install
+ them yourself. Over subsequent releases additional GitLab metrics will be
+ captured.
+- Prometheus services are off by default but will be on starting with GitLab 9.0.
+
+[Prometheus] is a powerful time-series monitoring service, providing a flexible
+platform for monitoring GitLab and other software products.
+GitLab provides out of the box monitoring with Prometheus, providing easy
+access to high quality time-series monitoring of GitLab services.
+
+## Overview
+
+Prometheus works by periodically connecting to data sources and collecting their
+performance metrics. To view and work with the monitoring data, you can either
+connect directly to Prometheus or utilize a dashboard tool like [Grafana].
+
+## Configuring Prometheus
+
+>**Note:**
+Available since Omnibus GitLab 8.16. For installations from source you'll
+have to install and configure it yourself.
+
+To enable Prometheus:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ prometheus['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+By default, Prometheus will run as the `gitlab-prometheus` user and listen on
+TCP port `9090` under localhost. If the [node exporter](#node-exporter) service
+has been enabled, it will automatically be set up as a monitoring target for
+Prometheus.
+
+## Viewing Performance Metrics
+
+After you have [enabled Prometheus](#configuring-prometheus), you can visit
+`<your_domain_name>:9090` for the dashboard that Prometheus offers by default.
+
+The performance data collected by Prometheus can be viewed directly in the
+Prometheus console or through a compatible dashboard tool.
+The Prometheus interface provides a [flexible query language][prom-query] to work
+with the collected data where you can visualize their output.
+For a more fully featured dashboard, Grafana can be used and has
+[official support for Prometheus][prom-grafana].
+
+## Prometheus exporters
+
+There are a number of libraries and servers which help in exporting existing
+metrics from third-party systems as Prometheus metrics. This is useful for cases
+where it is not feasible to instrument a given system with Prometheus metrics
+directly (for example, HAProxy or Linux system stats). You can read more in the
+[Prometheus exporters and integrations documentation][prom-exporters].
+
+While you can use any exporter you like with your GitLab installation, the
+following ones documented here are bundled in the Omnibus GitLab packages
+making it easy to configure and use.
+
+### Node exporter
+
+>**Note:**
+Available since Omnibus GitLab 8.16. For installations from source you'll
+have to install and configure it yourself.
+
+The [node exporter] allows you to measure various machine resources such as
+memory, disk and CPU utilization.
+
+To enable the node exporter:
+
+1. [Enable Prometheus](#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ node_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus it will now automatically begin collecting performance data from
+the node exporter. You can visit `<your_domain_name>:9100/metrics` for a real
+time representation of the metrics that are collected. Refresh the page and
+you will see the data change.
+
+[grafana]: https://grafana.net
+[node exporter]: https://github.com/prometheus/node_exporter
+[prometheus]: https://prometheus.io
+[prom-query]: https://prometheus.io/docs/querying/basics
+[prom-grafana]: https://prometheus.io/docs/visualization/grafana/
+[scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
+[prom-exporters]: https://prometheus.io/docs/instrumenting/exporters/
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index f3c2e72341f..33b9b28433a 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -27,6 +27,7 @@ Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
+Redis Version: 3.2.5
Sidekiq Version: 2.17.8
GitLab information
diff --git a/doc/api/README.md b/doc/api/README.md
index f65b934b9db..20f28e8d30e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -104,6 +104,13 @@ that needs access to the GitLab API.
Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header.
+> [Introduced][ce-5951] in GitLab 8.15.
+
+Personal Access Tokens can be created with one or more scopes that allow various actions
+that a given token can perform. Although there are only two scopes available at the
+moment – `read_user` and `api` – the groundwork has been laid to add more scopes easily.
+
+At any time you can revoke any personal access token by just clicking **Revoke**.
### Session Cookie
@@ -380,3 +387,4 @@ programming languages. Visit the [GitLab website] for a complete list.
[GitLab website]: https://about.gitlab.com/applications/#api-clients "Clients using the GitLab API"
[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/api/api.rb
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
+[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
diff --git a/doc/api/issues.md b/doc/api/issues.md
index dd84afd7c73..b276d1ad918 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -712,6 +712,146 @@ Example response:
}
```
+## Set a time estimate for an issue
+
+Sets an estimated time of work for this issue.
+
+```
+POST /projects/:id/issues/:issue_id/time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "3h 30m",
+ "human_total_time_spent": null,
+ "time_estimate": 12600,
+ "total_time_spent": 0
+}
+```
+
+## Reset the time estimate for an issue
+
+Resets the estimated time for this issue to 0 seconds.
+
+```
+POST /projects/:id/issues/:issue_id/reset_time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Add spent time for an issue
+
+Adds spent time for this issue
+
+```
+POST /projects/:id/issues/:issue_id/add_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": "1h",
+ "time_estimate": 0,
+ "total_time_spent": 3600
+}
+```
+
+## Reset spent time for an issue
+
+Resets the total spent time for this issue to 0 seconds.
+
+```
+POST /projects/:id/issues/:issue_id/reset_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Get time tracking stats
+
+```
+GET /projects/:id/issues/:issue_id/time_stats
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "2h",
+ "human_total_time_spent": "1h",
+ "time_estimate": 7200,
+ "total_time_spent": 3600
+}
+```
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 662cc9da733..7b005591545 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1018,3 +1018,142 @@ Example response:
}]
}
```
+## Set a time estimate for a merge request
+
+Sets an estimated time of work for this merge request.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "3h 30m",
+ "human_total_time_spent": null,
+ "time_estimate": 12600,
+ "total_time_spent": 0
+}
+```
+
+## Reset the time estimate for a merge request
+
+Resets the estimated time for this merge request to 0 seconds.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Add spent time for a merge request
+
+Adds spent time for this merge request
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/add_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge request |
+| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": "1h",
+ "time_estimate": 0,
+ "total_time_spent": 3600
+}
+```
+
+## Reset spent time for a merge request
+
+Resets the total spent time for this merge request to 0 seconds.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge_request |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "time_estimate": 0,
+ "total_time_spent": 0
+}
+```
+
+## Get time tracking stats
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/time_stats
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of a project's merge request |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats
+```
+
+Example response:
+
+```json
+{
+ "human_time_estimate": "2h",
+ "human_total_time_spent": "1h",
+ "time_estimate": 7200,
+ "total_time_spent": 3600
+}
+```
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 1d782200cca..869743ce80a 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -61,7 +61,18 @@ correctly with your CI builds:
1. First, make sure you have used [relative URLs](#configuring-the-gitmodules-file)
for the submodules located in the same GitLab server.
-1. Then, use `git submodule sync/update` in `before_script`:
+1. Next, if you are using `gitlab-ci-multi-runner` v1.10+, you can set the
+ `GIT_SUBMODULE_STRATEGY` variable to either `normal` or `recursive` to tell
+ the runner to fetch your submodules before the build:
+ ```yaml
+ variables:
+ GIT_SUBMODULE_STRATEGY: recursive
+ ```
+ See the [`.gitlab-ci.yml` reference](yaml/README.md#git-submodule-strategy)
+ for more details about `GIT_SUBMODULE_STRATEGY`.
+
+1. If you are using an older version of `gitlab-ci-multi-runner`, then use
+ `git submodule sync/update` in `before_script`:
```yaml
before_script:
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 7158b2e7895..75a0897eb15 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1034,6 +1034,41 @@ variables:
GIT_STRATEGY: none
```
+## Git Submodule Strategy
+
+> Requires GitLab Runner v1.10+.
+
+The `GIT_SUBMODULE_STRATEGY` variable is used to control if / how Git
+submodules are included when fetching the code before a build. Like
+`GIT_STRATEGY`, it can be set in either the global [`variables`](#variables)
+section or the [`variables`](#job-variables) section for individual jobs.
+
+There are three posible values: `none`, `normal`, and `recursive`:
+
+- `none` means that submodules will not be included when fetching the project
+ code. This is the default, which matches the pre-v1.10 behavior.
+
+- `normal` means that only the top-level submodules will be included. It is
+ equivalent to:
+ ```
+ $ git submodule sync
+ $ git submodule update --init
+ ```
+
+- `recursive` means that all submodules (including submodules of submodules)
+ will be included. It is equivalent to:
+ ```
+ $ git submodule sync --recursive
+ $ git submodule update --init --recursive
+ ```
+
+Note that for this feature to work correctly, the submodules must be configured
+(in `.gitmodules`) with either:
+- the HTTP(S) URL of a publicly-accessible repository, or
+- a relative path to another repository on the same GitLab server. See the
+ [Git submodules](../git_submodules.md) documentation.
+
+
## Build stages attempts
> Introduced in GitLab, it requires GitLab Runner v1.9+.
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 1ef34c79971..e4a0e0b92bc 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -9,7 +9,7 @@ code is effective, understandable, and maintainable.
Any developer can, and is encouraged to, perform code review on merge requests
of colleagues and contributors. However, the final decision to accept a merge
-request is up to one of our merge request "endbosses", denoted on the
+request is up to one the project's maintainers, denoted on the
[team page](https://about.gitlab.com/team).
## Everyone
@@ -81,15 +81,15 @@ balance in how deep the reviewer can interfere with the code created by a
reviewee.
- Learning how to find the right balance takes time; that is why we have
- minibosses that become merge request endbosses after some time spent on
- reviewing merge requests.
+ reviewers that become maintainers after some time spent on reviewing merge
+ requests.
- Finding bugs and improving code style is important, but thinking about good
design is important as well. Building abstractions and good design is what
makes it possible to hide complexity and makes future changes easier.
- Asking the reviewee to change the design sometimes means the complete rewrite
- of the contributed code. It's usually a good idea to ask another merge
- request endboss before doing it, but have the courage to do it when you
- believe it is important.
+ of the contributed code. It's usually a good idea to ask another maintainer or
+ reviewer before doing it, but have the courage to do it when you believe it is
+ important.
- There is a difference in doing things right and doing things right now.
Ideally, we should do the former, but in the real world we need the latter as
well. A good example is a security fix which should be released as soon as
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index 9e782ab977f..f79bd23dc90 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -297,16 +297,74 @@ For our currently-supported browsers, see our [requirements][requirements].
## Gotchas
-### Phantom.JS (used by Teaspoon & Rspec) chokes, returning vague JavaScript errors
-
-If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but
-can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't
-have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're
-working in (`git mv <file.js> <file.js.es6>`).
-
-Similar errors will be thrown if you're using
-any of the [array methods introduced in ES6](http://www.2ality.com/2014/05/es6-array-methods.html)
-whether or not you've updated the file extension.
+### Spec errors due to use of ES6 features in `.js` files
+
+If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
+thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually,
+you may have included `ES6`-style JavaScript in files that don't have the
+`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
+you're working in (`git mv <file.js> <file.js.es6>`).
+
+### Spec errors due to use of unsupported JavaScript
+
+Similar errors will be thrown if you're using JavaScript features not yet
+supported by our test runner's version of webkit, whether or not you've updated
+the file extension. Examples of unsupported JavaScript features are:
+
+- Array.from
+- Array.find
+- Array.first
+- Object.assign
+- Async functions
+- Generators
+- Array destructuring
+- For Of
+- Symbol/Symbol.iterator
+- Spread
+
+Until these are polyfilled or transpiled appropriately, they should not be used.
+Please update this list with additional unsupported features or when any of
+these are made usable.
+
+### Spec errors due to JavaScript not enabled
+
+If, as a result of a change you've made, a feature now depends on JavaScript to
+run correctly, you need to make sure a JavaScript web driver is enabled when
+specs are run. If you don't you'll see vague error messages from the spec
+runner, and an explosion of vague console errors in the HTML snapshot.
+
+To enable a JavaScript driver in an `rspec` test, add `js: true` to the
+individual spec or the context block containing multiple specs that need
+JavaScript enabled:
+
+```ruby
+
+# For one spec
+it 'presents information about abuse report', js: true do
+ # assertions...
+end
+
+describe "Admin::AbuseReports", js: true do
+ it 'presents information about abuse report' do
+ # assertions...
+ end
+ it 'shows buttons for adding to abuse report' do
+ # assertions...
+ end
+end
+```
+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"
+```
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 0363bf8c1d5..8232a0a113c 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -3,7 +3,7 @@
To ensure a merge request does not negatively impact performance of GitLab
_every_ merge request **must** adhere to the guidelines outlined in this
document. There are no exceptions to this rule unless specifically discussed
-with and agreed upon by merge request endbosses and performance specialists.
+with and agreed upon by backend maintainers and performance specialists.
To measure the impact of a merge request you can use
[Sherlock](profiling.md#sherlock). It's also highly recommended that you read
@@ -40,9 +40,9 @@ section below for more information.
about the impact.
Sometimes it's hard to assess the impact of a merge request. In this case you
-should ask one of the merge request (mini) endbosses to review your changes. You
-can find a list of these endbosses at <https://about.gitlab.com/team/>. An
-endboss in turn can request a performance specialist to review the changes.
+should ask one of the merge request reviewers to review your changes. You can
+find a list of these reviewers at <https://about.gitlab.com/team/>. A reviewer
+in turn can request a performance specialist to review the changes.
## Query Counts
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 9cebed34b7e..3e7674e13ab 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -124,7 +124,7 @@ Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
- echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+ echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 0c53584d201..af8a1c4e5ed 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -74,8 +74,10 @@ in the **Authorized applications** section under **Profile Settings > Applicatio
---
-As you can see, the default scope `api` is used, which is the only scope that
-GitLab supports so far. At any time you can revoke any access by just clicking
-**Revoke**.
+GitLab's OAuth applications support scopes, which allow various actions that any given
+application can perform. Although there are only two scopes available at the
+moment – `read_user` and `api` – the groundwork has been laid to add more scopes easily.
+
+At any time you can revoke any access by just clicking **Revoke**.
[oauth]: http://oauth.net/2/ "OAuth website"
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 4c933cef9b7..98a680d0dbe 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -41,6 +41,9 @@ that are in common for all providers that we need to consider.
- `allow_single_sign_on` allows you to specify the providers you want to allow to
automatically create an account. It defaults to `false`. If `false` users must
be created manually or they will not be able to sign in via OmniAuth.
+- `auto_link_ldap_user` can be used if you have [LDAP / ActiveDirectory](ldap.md)
+ integration enabled. It defaults to false. When enabled, users automatically
+ created through OmniAuth will be linked to their LDAP entry as well.
- `block_auto_created_users` defaults to `true`. If `true` auto created users will
be blocked by default and will have to be unblocked by an administrator before
they are able to sign in.
@@ -52,6 +55,10 @@ SAML, Shibboleth, Crowd or Google, or set it to `false` otherwise any user on
the Internet will be able to successfully sign in to your GitLab without
administrative approval.
+>**Note:**
+`auto_link_ldap_user` requires the `uid` of the user to be the same in both LDAP
+and the OmniAuth provider.
+
To change these settings:
* **For omnibus package**
@@ -72,6 +79,7 @@ To change these settings:
# using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none.
# User accounts will be created automatically when authentication was successful.
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml', 'twitter']
+ gitlab_rails['omniauth_auto_link_ldap_user'] = true
gitlab_rails['omniauth_block_auto_created_users'] = true
```
@@ -99,6 +107,8 @@ To change these settings:
# User accounts will be created automatically when authentication was successful.
allow_single_sign_on: ["saml", "twitter"]
+ auto_link_ldap_user: true
+
# Locks down those users until they have been cleared by the admin (default: true).
block_auto_created_users: true
```
diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png
deleted file mode 100644
index b224ab14195..00000000000
--- a/doc/profile/2fa_u2f_authenticate.png
+++ /dev/null
Binary files differ
diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md
index 3f6dfe03d14..60918a0339c 100644
--- a/doc/profile/two_factor_authentication.md
+++ b/doc/profile/two_factor_authentication.md
@@ -1,143 +1 @@
-# Two-factor Authentication (2FA)
-
-Two-factor Authentication (2FA) provides an additional level of security to your
-GitLab account. Once enabled, in addition to supplying your username and
-password to login, you'll be prompted for a code generated by an application on
-your phone.
-
-By enabling 2FA, the only way someone other than you can log into your account
-is to know your username and password *and* have access to your phone.
-
-> **Note:**
-When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you
-lose your codes for GitLab.com, we can't disable or recover them.
-
-In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
-the second factor of authentication. Once enabled, in addition to supplying your username and
-password to login, you'll be prompted to activate your U2F device (usually by pressing
-a button on it), and it will perform secure authentication on your behalf.
-
-> **Note:** Support for U2F devices was added in version 8.8
-
-The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
-that you set up both methods of two-factor authentication, so you can still access your account
-from other browsers.
-
-> **Note:** GitLab officially only supports [Yubikey] U2F devices.
-
-## Enabling 2FA
-
-### Enable 2FA via mobile application
-
-**In GitLab:**
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Account**.
-1. Click **Enable Two-factor Authentication**.
-
-![Two-factor setup](2fa.png)
-
-**On your phone:**
-
-1. Install a compatible application. We recommend [Google Authenticator]
-\(proprietary\) or [FreeOTP] \(open source\).
-1. In the application, add a new entry in one of two ways:
- * Scan the code with your phone's camera to add the entry automatically.
- * Enter the details provided to add the entry manually.
-
-**In GitLab:**
-
-1. Enter the six-digit pin number from the entry on your phone into the **Pin
- code** field.
-1. Click **Submit**.
-
-If the pin you entered was correct, you'll see a message indicating that
-Two-Factor Authentication has been enabled, and you'll be presented with a list
-of recovery codes.
-
-### Enable 2FA via U2F device
-
-**In GitLab:**
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Account**.
-1. Click **Enable Two-Factor Authentication**.
-1. Plug in your U2F device.
-1. Click on **Setup New U2F Device**.
-1. A light will start blinking on your device. Activate it by pressing its button.
-
-You will see a message indicating that your device was successfully set up.
-Click on **Register U2F Device** to complete the process.
-
-![Two-Factor U2F Setup](2fa_u2f_register.png)
-
-## Recovery Codes
-
-Should you ever lose access to your phone, you can use one of the ten provided
-backup codes to login to your account. We suggest copying or printing them for
-storage in a safe place. **Each code can be used only once** to log in to your
-account.
-
-If you lose the recovery codes or just want to generate new ones, you can do so
-from the **Profile Settings** > **Account** page where you first enabled 2FA.
-
-> **Note:** Recovery codes are not generated for U2F devices.
-
-## Logging in with 2FA Enabled
-
-Logging in with 2FA enabled is only slightly different than a normal login.
-Enter your username and password credentials as you normally would, and you'll
-be presented with a second prompt, depending on which type of 2FA you've enabled.
-
-### Log in via mobile application
-
-Enter the pin from your phone's application or a recovery code to log in.
-
-![Two-Factor Authentication on sign in via OTP](2fa_auth.png)
-
-### Log in via U2F device
-
-1. Click **Login via U2F Device**
-1. A light will start blinking on your device. Activate it by pressing its button.
-
-You will see a message indicating that your device responded to the authentication request.
-Click on **Authenticate via U2F Device** to complete the process.
-
-![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png)
-
-## Disabling 2FA
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Account**.
-1. Click **Disable**, under **Two-Factor Authentication**.
-
-This will clear all your two-factor authentication registrations, including mobile
-applications and U2F devices.
-
-## Personal access tokens
-
-When 2FA is enabled, you can no longer use your normal account password to
-authenticate with Git over HTTPS on the command line, you must use a personal
-access token instead.
-
-1. Log in to your GitLab account.
-1. Go to your **Profile Settings**.
-1. Go to **Access Tokens**.
-1. Choose a name and expiry date for the token.
-1. Click on **Create Personal Access Token**.
-1. Save the personal access token somewhere safe.
-
-When using git over HTTPS on the command line, enter the personal access token
-into the password field.
-
-## Note to GitLab administrators
-
-You need to take special care to that 2FA keeps working after
-[restoring a GitLab backup](../raketasks/backup_restore.md).
-
-[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
-[FreeOTP]: https://fedorahosted.org/freeotp/
-[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
+This document was moved to [user/profile/account](../user/profile/account/two_factor_authentication.md).
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
index 59d5da702f8..99aa9e44bdb 100644
--- a/doc/project_services/kubernetes.md
+++ b/doc/project_services/kubernetes.md
@@ -8,7 +8,7 @@ the [configuration](#configuration) section.
If you have a single cluster that you want to use for all your projects,
you can pre-fill the settings page with a default template. To configure the
-template, see the [Services Templates](services-templates.md) document.
+template, see the [Services Templates](services_templates.md) document.
## Configuration
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 0f398874b8f..547d855d777 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -46,6 +46,7 @@ further configuration instructions and details. Contributions are welcome.
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
+| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
diff --git a/doc/public_access/img/restrict_visibility_levels.png b/doc/public_access/img/restrict_visibility_levels.png
new file mode 100644
index 00000000000..c7d4d87981f
--- /dev/null
+++ b/doc/public_access/img/restrict_visibility_levels.png
Binary files differ
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index a3921f1b89f..e8f4c73120c 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -52,7 +52,7 @@ for anonymous users. The group page now has a visibility level icon.
## Visibility of users
-The public page of a user, located at `/u/username`, is always visible whether
+The public page of a user, located at `/username`, is always visible whether
you are logged in or not.
When visiting the public page of a user, you can only see the projects which
@@ -60,10 +60,13 @@ you are privileged to.
If the public level is restricted, user profiles are only visible to logged in users.
-
## Restricting the use of public or internal projects
In the Admin area under **Settings** (`/admin/application_settings`), you can
restrict the use of visibility levels for users when they create a project or a
-snippet. This is useful to prevent people exposing their repositories to public
+snippet:
+
+![Restrict visibility levels](img/restrict_visibility_levels.png)
+
+This is useful to prevent people exposing their repositories to public
by accident. The restricted visibility settings do not apply to admin users.
diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md
index 3d68fe201a7..63f3c3fdda9 100644
--- a/doc/update/8.15-to-8.16.md
+++ b/doc/update/8.15-to-8.16.md
@@ -36,7 +36,7 @@ Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
-echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
diff --git a/doc/user/account/security.md b/doc/user/account/security.md
index 816094bf8d2..9336dee7451 100644
--- a/doc/user/account/security.md
+++ b/doc/user/account/security.md
@@ -1,3 +1 @@
-# Account Security
-
-- [Two-Factor Authentication](two_factor_authentication.md)
+This document was moved to [profile](../profile/index.md#security).
diff --git a/doc/user/account/two_factor_authentication.md b/doc/user/account/two_factor_authentication.md
index 881358ed94d..ea2c8307860 100644
--- a/doc/user/account/two_factor_authentication.md
+++ b/doc/user/account/two_factor_authentication.md
@@ -1,68 +1 @@
-# Two-Factor Authentication
-
-## Recovery options
-
-If you lose your code generation device (such as your mobile phone) and you need
-to disable two-factor authentication on your account, you have several options.
-
-### Use a saved recovery code
-
-When you enabled two-factor authentication for your account, a series of
-recovery codes were generated. If you saved those codes somewhere safe, you
-may use one to sign in.
-
-First, enter your username/email and password on the GitLab sign in page. When
-prompted for a two-factor code, enter one of the recovery codes you saved
-previously.
-
-> **Note:** Once a particular recovery code has been used, it cannot be used again.
- You may still use the other saved recovery codes at a later time.
-
-### Generate new recovery codes using SSH
-
-It's not uncommon for users to forget to save the recovery codes when enabling
-two-factor authentication. If you have an SSH key added to your GitLab account,
-you can generate a new set of recovery codes using SSH.
-
-Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to
-confirm that you wish to generate new codes. If you choose to continue, any
-previously saved codes will be invalidated.
-
-```bash
-$ ssh git@gitlab.example.com 2fa_recovery_codes
-Are you sure you want to generate new two-factor recovery codes?
-Any existing recovery codes you saved will be invalidated. (yes/no)
-yes
-
-Your two-factor authentication recovery codes are:
-
-119135e5a3ebce8e
-11f6v2a498810dcd
-3924c7ab2089c902
-e79a3398bfe4f224
-34bd7b74adbc8861
-f061691d5107df1a
-169bf32a18e63e7f
-b510e7422e81c947
-20dbed24c5e74663
-df9d3b9403b9c9f0
-
-During sign in, use one of the codes above when prompted for
-your two-factor code. Then, visit your Profile Settings and add
-a new device so you do not lose access to your account again.
-```
-
-Next, go to the GitLab sign in page and enter your username/email and password.
-When prompted for a two-factor code, enter one of the recovery codes obtained
-from the command line output.
-
-> **Note:** After signing in, you should immediately visit your **Profile Settings
- -> Account** to set up two-factor authentication with a new device.
-
-### Ask a GitLab administrator to disable two-factor on your account
-
-If the above two methods are not possible, you may ask a GitLab global
-administrator to disable two-factor authentication for your account. Please
-be aware that this will temporarily leave your account in a less secure state.
-You should sign in and re-enable two-factor authentication as soon as possible
-after the administrator disables it.
+This document was moved to [profile/account/two_factor_authentication](../profile/account/two_factor_authentication.md).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index f6484688721..008872b59a7 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -300,6 +300,20 @@ You can add task lists to issues, merge requests and comments. To create a task
- [x] Sub-task 2
- [ ] Sub-task 3
+Tasks formatted as ordered lists are supported as well:
+
+```no-highlight
+1. [x] Completed task
+1. [ ] Incomplete task
+ 1. [ ] Sub-task 1
+ 1. [x] Sub-task 2
+```
+
+1. [x] Completed task
+1. [ ] Incomplete task
+ 1. [ ] Sub-task 1
+ 1. [x] Sub-task 2
+
Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
### Videos
@@ -650,7 +664,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-This line is also a separate paragraph, and...
+This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
```
@@ -662,7 +676,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also begins a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
-This line is also a separate paragraph, and...
+This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
@@ -800,4 +814,4 @@ A link starting with a `/` is relative to the wiki root.
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
-[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" \ No newline at end of file
+[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 5ada8748d85..678fc3ffd1f 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -19,10 +19,12 @@ The following table depicts the various user permission levels in a project.
| Action | Guest | Reporter | Developer | Master | Owner |
|---------------------------------------|---------|------------|-------------|----------|--------|
| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View confidential issues | (✓) [^1] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
-| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| See a list of builds | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
+| See a build log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
+| Download and browse build artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ |
@@ -63,11 +65,8 @@ The following table depicts the various user permission levels in a project.
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
-| Force push to protected branches [^2] | | | | | |
-| Remove protected branches [^2] | | | | | |
-
-[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
-[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
+| Force push to protected branches [^3] | | | | | |
+| Remove protected branches [^3] | | | | | |
## Group
@@ -156,17 +155,20 @@ users:
| Run CI build | | ✓ | ✓ | ✓ |
| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
-| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ |
-| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Clone source and LFS from internal projects | | ✓ [^4] | ✓ [^4] | ✓ |
+| Clone source and LFS from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
| Push source and LFS | | | | |
| Pull container images from current project | | ✓ | ✓ | ✓ |
| Pull container images from public projects | | ✓ | ✓ | ✓ |
-| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ |
-| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Pull container images from internal projects| | ✓ [^4] | ✓ [^4] | ✓ |
+| Pull container images from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
| Push container images to current project | | ✓ | ✓ | ✓ |
| Push container images to other projects | | | | |
-[^3]: Only if user is not external one.
-[^4]: Only if user is a member of the project.
+[^1]: Guest users can only view the confidential issues they created themselves
+[^2]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
+[^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner
+[^4]: Only if user is not external one.
+[^5]: Only if user is a member of the project.
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/profile/2fa.png b/doc/user/profile/account/img/2fa.png
index bb464efa685..bb464efa685 100644
--- a/doc/profile/2fa.png
+++ b/doc/user/profile/account/img/2fa.png
Binary files differ
diff --git a/doc/profile/2fa_auth.png b/doc/user/profile/account/img/2fa_auth.png
index 0caaed10805..0caaed10805 100644
--- a/doc/profile/2fa_auth.png
+++ b/doc/user/profile/account/img/2fa_auth.png
Binary files differ
diff --git a/doc/user/profile/account/img/2fa_u2f_authenticate.png b/doc/user/profile/account/img/2fa_u2f_authenticate.png
new file mode 100644
index 00000000000..ff2e936764d
--- /dev/null
+++ b/doc/user/profile/account/img/2fa_u2f_authenticate.png
Binary files differ
diff --git a/doc/profile/2fa_u2f_register.png b/doc/user/profile/account/img/2fa_u2f_register.png
index 1cc142aa851..1cc142aa851 100644
--- a/doc/profile/2fa_u2f_register.png
+++ b/doc/user/profile/account/img/2fa_u2f_register.png
Binary files differ
diff --git a/doc/user/profile/account/index.md b/doc/user/profile/account/index.md
new file mode 100644
index 00000000000..764354e1e96
--- /dev/null
+++ b/doc/user/profile/account/index.md
@@ -0,0 +1,5 @@
+# Profile settings
+
+## Account
+
+Set up [two-factor authentication](two_factor_authentication.md).
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
new file mode 100644
index 00000000000..0f959b956a5
--- /dev/null
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -0,0 +1,215 @@
+# Two-Factor Authentication
+
+Two-factor Authentication (2FA) provides an additional level of security to your
+GitLab account. Once enabled, in addition to supplying your username and
+password to login, you'll be prompted for a code generated by an application on
+your phone.
+
+By enabling 2FA, the only way someone other than you can log into your account
+is to know your username and password *and* have access to your phone.
+
+## Overview
+
+> **Note:**
+When you enable 2FA, don't forget to back up your recovery codes.
+
+In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
+the second factor of authentication. Once enabled, in addition to supplying your username and
+password to login, you'll be prompted to activate your U2F device (usually by pressing
+a button on it), and it will perform secure authentication on your behalf.
+
+The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
+that you set up both methods of two-factor authentication, so you can still access your account
+from other browsers.
+
+## Enabling 2FA
+
+There are two ways to enable two-factor authentication: via a mobile application
+or a U2F device.
+
+### Enable 2FA via mobile application
+
+**In GitLab:**
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Account**.
+1. Click **Enable Two-factor Authentication**.
+
+![Two-factor setup](img/2fa.png)
+
+**On your phone:**
+
+1. Install a compatible application. We recommend [Google Authenticator]
+\(proprietary\) or [FreeOTP] \(open source\).
+1. In the application, add a new entry in one of two ways:
+ * Scan the code with your phone's camera to add the entry automatically.
+ * Enter the details provided to add the entry manually.
+
+**In GitLab:**
+
+1. Enter the six-digit pin number from the entry on your phone into the **Pin
+ code** field.
+1. Click **Submit**.
+
+If the pin you entered was correct, you'll see a message indicating that
+Two-Factor Authentication has been enabled, and you'll be presented with a list
+of recovery codes.
+
+### Enable 2FA via U2F device
+
+> **Notes:**
+- GitLab officially only supports [Yubikey] U2F devices.
+- Support for U2F devices was added in GitLab 8.8.
+
+**In GitLab:**
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Account**.
+1. Click **Enable Two-Factor Authentication**.
+1. Plug in your U2F device.
+1. Click on **Setup New U2F Device**.
+1. A light will start blinking on your device. Activate it by pressing its button.
+
+You will see a message indicating that your device was successfully set up.
+Click on **Register U2F Device** to complete the process.
+
+![Two-Factor U2F Setup](img/2fa_u2f_register.png)
+
+## Recovery Codes
+
+> **Note:**
+Recovery codes are not generated for U2F devices.
+
+Should you ever lose access to your phone, you can use one of the ten provided
+backup codes to login to your account. We suggest copying or printing them for
+storage in a safe place. **Each code can be used only once** to log in to your
+account.
+
+If you lose the recovery codes or just want to generate new ones, you can do so
+from the **Profile settings âž” Account** page where you first enabled 2FA.
+
+## Logging in with 2FA Enabled
+
+Logging in with 2FA enabled is only slightly different than a normal login.
+Enter your username and password credentials as you normally would, and you'll
+be presented with a second prompt, depending on which type of 2FA you've enabled.
+
+### Log in via mobile application
+
+Enter the pin from your phone's application or a recovery code to log in.
+
+![Two-Factor Authentication on sign in via OTP](img/2fa_auth.png)
+
+### Log in via U2F device
+
+1. Click **Login via U2F Device**
+1. A light will start blinking on your device. Activate it by pressing its button.
+
+You will see a message indicating that your device responded to the authentication request.
+Click on **Authenticate via U2F Device** to complete the process.
+
+![Two-Factor Authentication on sign in via U2F device](img/2fa_u2f_authenticate.png)
+
+## Disabling 2FA
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Account**.
+1. Click **Disable**, under **Two-Factor Authentication**.
+
+This will clear all your two-factor authentication registrations, including mobile
+applications and U2F devices.
+
+## Personal access tokens
+
+When 2FA is enabled, you can no longer use your normal account password to
+authenticate with Git over HTTPS on the command line, you must use a personal
+access token instead.
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Access Tokens**.
+1. Choose a name and expiry date for the token.
+1. Click on **Create Personal Access Token**.
+1. Save the personal access token somewhere safe.
+
+When using Git over HTTPS on the command line, enter the personal access token
+into the password field.
+
+## Recovery options
+
+If you lose your code generation device (such as your mobile phone) and you need
+to disable two-factor authentication on your account, you have several options.
+
+### Use a saved recovery code
+
+When you enabled two-factor authentication for your account, a series of
+recovery codes were generated. If you saved those codes somewhere safe, you
+may use one to sign in.
+
+First, enter your username/email and password on the GitLab sign in page. When
+prompted for a two-factor code, enter one of the recovery codes you saved
+previously.
+
+> **Note:** Once a particular recovery code has been used, it cannot be used again.
+ You may still use the other saved recovery codes at a later time.
+
+### Generate new recovery codes using SSH
+
+It's not uncommon for users to forget to save the recovery codes when enabling
+two-factor authentication. If you have an SSH key added to your GitLab account,
+you can generate a new set of recovery codes using SSH.
+
+Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to
+confirm that you wish to generate new codes. If you choose to continue, any
+previously saved codes will be invalidated.
+
+```bash
+$ ssh git@gitlab.example.com 2fa_recovery_codes
+Are you sure you want to generate new two-factor recovery codes?
+Any existing recovery codes you saved will be invalidated. (yes/no)
+yes
+
+Your two-factor authentication recovery codes are:
+
+119135e5a3ebce8e
+11f6v2a498810dcd
+3924c7ab2089c902
+e79a3398bfe4f224
+34bd7b74adbc8861
+f061691d5107df1a
+169bf32a18e63e7f
+b510e7422e81c947
+20dbed24c5e74663
+df9d3b9403b9c9f0
+
+During sign in, use one of the codes above when prompted for
+your two-factor code. Then, visit your Profile Settings and add
+a new device so you do not lose access to your account again.
+```
+
+Next, go to the GitLab sign in page and enter your username/email and password.
+When prompted for a two-factor code, enter one of the recovery codes obtained
+from the command line output.
+
+> **Note:** After signing in, you should immediately visit your **Profile Settings
+ -> Account** to set up two-factor authentication with a new device.
+
+### Ask a GitLab administrator to disable two-factor on your account
+
+If the above two methods are not possible, you may ask a GitLab global
+administrator to disable two-factor authentication for your account. Please
+be aware that this will temporarily leave your account in a less secure state.
+You should sign in and re-enable two-factor authentication as soon as possible
+after the administrator disables it.
+
+## Note to GitLab administrators
+
+You need to take special care to that 2FA keeps working after
+[restoring a GitLab backup](../../../raketasks/backup_restore.md).
+
+[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
+[FreeOTP]: https://fedorahosted.org/freeotp/
+[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md
new file mode 100644
index 00000000000..1760b182114
--- /dev/null
+++ b/doc/user/project/issues/confidential_issues.md
@@ -0,0 +1,68 @@
+# Confidential issues
+
+> [Introduced][ce-3282] in GitLab 8.6.
+
+Confidential issues are issues visible only to members of a project with
+[sufficient permissions](#permissions-and-access-to-confidential-issues).
+Confidential issues can be used by open source projects and companies alike to
+keep security vulnerabilities private or prevent surprises from leaking out.
+
+## Making an issue confidential
+
+You can make an issue confidential either by creating a new issue or editing
+an existing one.
+
+When you create a new issue, a checkbox right below the text area is available
+to mark the issue as confidential. Check that box and hit the **Submit issue**
+button to create the issue. For existing issues, edit them, check the
+confidential checkbox and hit **Save changes**.
+
+![Creating a new confidential issue](img/confidential_issues_create.png)
+
+## Making an issue non-confidential
+
+To make an issue non-confidential, all you have to do is edit it and unmark
+the confidential checkbox. Once you save the issue, it will gain the default
+visibility level you have chosen for your project.
+
+Every change from regular to confidential and vice versa, is indicated by a
+system note in the issue's comments.
+
+![Confidential issues system notes](img/confidential_issues_system_notes.png)
+
+## Indications of a confidential issue
+
+>**Note:** If you don't have [enough permissions](#permissions-and-access-to-confidential-issues),
+you won't be able to see the confidential issues at all.
+
+There are a few things that visually separate a confidential issue from a
+regular one. In the issues index page view, you can see the eye-slash icon
+next to the issues that are marked as confidential.
+
+![Confidential issues index page](img/confidential_issues_index_page.png)
+
+---
+
+Likewise, while inside the issue, you can see the eye-slash icon right next to
+the issue number, but there is also an indicator in the comment area that the
+issue you are commenting on is confidential.
+
+![Confidential issue page](img/confidential_issues_issue_page.png)
+
+## Permissions and access to confidential issues
+
+There are two kinds of level access for confidential issues. The general rule
+is that confidential issues are visible only to members of a project with at
+least [Reporter access][permissions]. However, a guest user can also create
+confidential issues, but can only view the ones that they created themselves.
+
+Confidential issues are also hidden in search results for unprivileged users.
+For example, here's what a user with Master and Guest access sees in the
+project's search results respectively.
+
+| Master access | Guest access |
+| :-----------: | :----------: |
+| ![Confidential issues search master](img/confidential_issues_search_master.png) | ![Confidential issues search guest](img/confidential_issues_search_guest.png) |
+
+[permissions]: ../../permissions.md#project
+[ce-3282]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3282
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
new file mode 100644
index 00000000000..b516d47ffa3
--- /dev/null
+++ b/doc/user/project/issues/due_dates.md
@@ -0,0 +1,37 @@
+# Due dates
+
+> [Introduced][ce-3614] in GitLab 8.7.
+
+Due dates can be used in issues to keep track of deadlines and make sure
+features are shipped on time. Due dates require at least [Reporter permissions][permissions]
+to be able to edit them. On the contrary, they can be seen by everybody.
+
+## Setting a due date
+
+When creating or editing an issue, you can see the due date field from where
+a calendar will appear to help you choose the date you want. To remove it,
+select the date text and delete it.
+
+![Create a due date](img/due_dates_create.png)
+
+A quicker way to set a due date is via the issue sidebar. Simply expand the
+sidebar and select **Edit** to pick a due date or remove the existing one.
+Changes are saved immediately.
+
+![Edit a due date via the sidebar](img/due_dates_edit_sidebar.png)
+
+## Making use of due dates
+
+Issues that have a due date can be distinctively seen in the issues index page
+with a calendar icon next to them. Issues where the date is past due will have
+the icon and the date colored red. You can sort issues by those that are
+_Due soon_ or _Due later_ from the dropdown menu in the right.
+
+![Issues with due dates in the issues index page](img/due_dates_issues_index_page.png)
+
+Due dates also appear in your [todos list](../../../workflow/todos.md).
+
+![Issues with due dates in the todos](img/due_dates_todos.png)
+
+[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
+[permissions]: ../../permissions.md#project
diff --git a/doc/user/project/issues/img/confidential_issues_create.png b/doc/user/project/issues/img/confidential_issues_create.png
new file mode 100644
index 00000000000..d259255599d
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_create.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_index_page.png b/doc/user/project/issues/img/confidential_issues_index_page.png
new file mode 100644
index 00000000000..042461e2451
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_index_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png
new file mode 100644
index 00000000000..b3568e9303a
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_issue_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_search_guest.png b/doc/user/project/issues/img/confidential_issues_search_guest.png
new file mode 100644
index 00000000000..b85de90b4d5
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_search_guest.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_search_master.png b/doc/user/project/issues/img/confidential_issues_search_master.png
new file mode 100644
index 00000000000..bf2b9428875
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_search_master.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_system_notes.png b/doc/user/project/issues/img/confidential_issues_system_notes.png
new file mode 100644
index 00000000000..4005f9350f7
--- /dev/null
+++ b/doc/user/project/issues/img/confidential_issues_system_notes.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_create.png b/doc/user/project/issues/img/due_dates_create.png
new file mode 100644
index 00000000000..d2fe1172bab
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_create.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_edit_sidebar.png b/doc/user/project/issues/img/due_dates_edit_sidebar.png
new file mode 100644
index 00000000000..6b37150e7db
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_edit_sidebar.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_issues_index_page.png b/doc/user/project/issues/img/due_dates_issues_index_page.png
new file mode 100644
index 00000000000..defcd5eca39
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_issues_index_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/due_dates_todos.png b/doc/user/project/issues/img/due_dates_todos.png
new file mode 100644
index 00000000000..92c9fd4021b
--- /dev/null
+++ b/doc/user/project/issues/img/due_dates_todos.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_conflict_editor.png b/doc/user/project/merge_requests/img/merge_conflict_editor.png
new file mode 100644
index 00000000000..6660920c191
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_conflict_editor.png
Binary files differ
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index 4d7225bd820..68c49054e47 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -21,6 +21,18 @@ request into the source branch, resolving the conflicts using the options
chosen. If the source branch is `feature` and the target branch is `master`,
this is similar to performing `git checkout feature; git merge master` locally.
+## Merge conflict editor
+
+> Introduced in GitLab 8.13.
+
+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.
+
+![Merge conflict editor](img/merge_conflict_editor.png)
+
## Conflicts available for resolution
GitLab allows resolving conflicts in a file where all of the below are true:
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index 0a66e9a3e15..a6546cffce2 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -30,3 +30,7 @@ do.
| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
| `/remove_due_date` | Remove due date |
| `/wip` | Toggle the Work In Progress status |
+| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
+| `/remove_estimate` | Remove estimated time |
+| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time |
+| `/remove_time_spent` | Remove time spent |
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 59a806de210..0b6f00c6aa4 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -7,6 +7,10 @@
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md)
+- Issues - The GitLab Issue Tracker is an advanced and complete tool for
+ tracking the evolution of a new idea or the process of solving a problem.
+ - [Confidential issues](../user/project/issues/confidential_issues.md)
+ - [Due date for issues](../user/project/issues/due_dates.md)
- [Issue Board](../user/project/issue_board.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
@@ -19,6 +23,7 @@
- [Slash commands](../user/project/slash_commands.md)
- [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
+- [Time tracking](time_tracking.md)
- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 423b095e69e..7a3628a39d7 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -79,7 +79,7 @@ Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
initial translation of existing SVN revisions into the Git repository:
```
-subgit install $GIT_REPOS_PATH
+subgit install $GIT_REPO_PATH
```
After the initial translation is completed, the Git repository and the SVN
diff --git a/doc/workflow/time-tracking/time-tracking-example.png b/doc/workflow/time-tracking/time-tracking-example.png
new file mode 100644
index 00000000000..bbcabb602d6
--- /dev/null
+++ b/doc/workflow/time-tracking/time-tracking-example.png
Binary files differ
diff --git a/doc/workflow/time-tracking/time-tracking-sidebar.png b/doc/workflow/time-tracking/time-tracking-sidebar.png
new file mode 100644
index 00000000000..d1ff5571f95
--- /dev/null
+++ b/doc/workflow/time-tracking/time-tracking-sidebar.png
Binary files differ
diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md
new file mode 100644
index 00000000000..de12994c516
--- /dev/null
+++ b/doc/workflow/time_tracking.md
@@ -0,0 +1,73 @@
+# Time Tracking
+
+> Introduced in GitLab 8.14.
+
+Time Tracking allows you to track estimates and time spent on issues and merge
+requests within GitLab.
+
+## Overview
+
+Time Tracking lets you:
+* record the time spent working on an issue or a merge request,
+* add an estimate of the amount of time needed to complete an issue or a merge
+request.
+
+You don't have to indicate an estimate to enter the time spent, and vice versa.
+
+Data about time tracking is shown on the issue/merge request sidebar, as shown
+below.
+
+![Time tracking in the sidebar](time-tracking/time-tracking-sidebar.png)
+
+## How to enter data
+
+Time Tracking uses two [slash commands] that GitLab introduced with this new
+feature: `/spend` and `/estimate`.
+
+Slash commands can be used in the body of an issue or a merge request, but also
+in a comment in both an issue or a merge request.
+
+Below is an example of how you can use those new slash commands inside a comment.
+
+![Time tracking example in a comment](time-tracking/time-tracking-example.png)
+
+Adding time entries (time spent or estimates) is limited to project members.
+
+### Estimates
+
+To enter an estimate, write `/estimate`, followed by the time. For example, if
+you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write
+`/estimate 3d 5h 10m`.
+
+Every time you enter a new time estimate, any previous time estimates will be
+overridden by this new value. There should only be one valid estimate in an
+issue or a merge request.
+
+To remove an estimation entirely, use `/remove_estimation`.
+
+### Time spent
+
+To enter a time spent, use `/spend 3d 5h 10m`.
+
+Every new time spent entry will be added to the current total time spent for the
+issue or the merge request.
+
+You can remove time by entering a negative amount: `/spend -3d` will remove 3
+days from the total time spent. You can't go below 0 minutes of time spent,
+so GitLab will automatically reset the time spent if you remove a larger amount
+of time compared to the time that was entered already.
+
+To remove all the time spent at once, use `/remove_time_spent`.
+
+## Configuration
+
+The following time units are available:
+* weeks (w)
+* days (d)
+* hours (h)
+* minutes (m)
+
+Default conversion rates are 1w = 5d and 1d = 8h.
+
+[landing]: https://about.gitlab.com/features/time-tracking
+[slash-commands]: ../user/project/slash_commands.md
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 57dda9c2234..d033e6b167b 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -39,10 +39,10 @@ Feature: Project Active Tab
# Sub Tabs: Settings
- Scenario: On Project Settings/Hooks
+ Scenario: On Project Settings/Integrations
Given I visit my project's settings page
- And I click the "Hooks" tab
- Then the active sub nav should be Hooks
+ And I click the "Integrations" tab
+ Then the active sub nav should be Integrations
And no other sub navs should be active
And the active main tab should be Settings
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index b2bec369e0f..33a1c88e33c 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -35,7 +35,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
step 'I have group with projects' do
@group = create(:group)
- @project = create(:project, namespace: @group)
+ @project = create(:empty_project, namespace: @group)
@event = create(:closed_issue_event, project: @project)
@project.team << [current_user, :master]
@@ -54,8 +54,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'group has a projects that does not belongs to me' do
- @forbidden_project1 = create(:project, group: @group)
- @forbidden_project2 = create(:project, group: @group)
+ @forbidden_project1 = create(:empty_project, group: @group)
+ @forbidden_project2 = create(:empty_project, group: @group)
end
step 'I should see 1 project at group list' do
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 39c65bb6cde..4e15d79ae74 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -79,13 +79,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
def project
@project ||= begin
- project = create :project
+ project = create(:empty_project)
project.team << [current_user, :master]
project
end
end
def public_project
- @public_project ||= create :project, :public
+ @public_project ||= create(:empty_project, :public)
end
end
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 6777101fb15..909ffec3646 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -105,14 +105,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
def project
@project ||= begin
- project = create :project
+ project = create(:project, :repository)
project.team << [current_user, :master]
project
end
end
def public_project
- @public_project ||= create :project, :public
+ @public_project ||= create(:project, :public, :repository)
end
def forked_project
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index c1d1eca9116..70e23098dde 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -104,7 +104,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
group = owned_group
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
- project = create :project, path: path, group: group
+ project = create(:empty_project, path: path, group: group)
milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 0c88838767c..4dc87dc4d9c 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -109,7 +109,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned')
- @archived_project = create(:project, namespace: group, archived: true, path: "archived-project")
+ @archived_project = create(:empty_project, :archived, namespace: group, path: "archived-project")
end
step 'I should see "archived" label' do
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index ea480d2ad68..24cfbaad7fe 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -162,7 +162,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I have group with projects' do
@group = create(:group)
@group.add_owner(current_user)
- @project = create(:project, namespace: @group)
+ @project = create(:project, :repository, namespace: @group)
@event = create(:closed_issue_event, project: @project)
@project.team << [current_user, :master]
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 58225032859..9f701840f1d 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -27,8 +27,8 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
end
- step 'I click the "Hooks" tab' do
- click_link('Webhooks')
+ step 'I click the "Integrations" tab' do
+ click_link('Integrations')
end
step 'I click the "Deploy Keys" tab' do
@@ -39,8 +39,8 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
ensure_active_sub_nav('Members')
end
- step 'the active sub nav should be Hooks' do
- ensure_active_sub_nav('Webhooks')
+ step 'the active sub nav should be Integrations' do
+ ensure_active_sub_nav('Integrations')
end
step 'the active sub nav should be Deploy Keys' do
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index 83b9ef48392..edf78f62f9a 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -46,11 +46,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'other projects have deploy keys' do
- @second_project = create(:project, namespace: create(:group))
+ @second_project = create(:empty_project, namespace: create(:group))
@second_project.team << [current_user, :master]
create(:deploy_keys_project, project: @second_project)
- @third_project = create(:project, namespace: create(:group))
+ @third_project = create(:empty_project, namespace: create(:group))
@third_project.team << [current_user, :master]
create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first)
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 70dbd030003..9a6c04fba7a 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -9,7 +9,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I am a member of project "Shop"' do
- @project = create(:project, name: "Shop")
+ @project = create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter]
end
@@ -18,7 +18,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I already have a project named "Shop" in my namespace' do
- @my_project = create(:project, name: "Shop", namespace: current_user.namespace)
+ @my_project = create(:project, :repository, name: "Shop", namespace: current_user.namespace)
end
step 'I should see a "Name has already been taken" warning' do
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 6c14d835004..c0827ff8fc7 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -7,7 +7,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
step 'I am a member of project "Shop"' do
@project = Project.find_by(name: "Shop")
- @project ||= create(:project, name: "Shop")
+ @project ||= create(:project, :repository, name: "Shop")
@project.team << [@user, :reporter]
end
diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb
index 13c0713669a..37b608ffbd3 100644
--- a/features/steps/project/hooks.rb
+++ b/features/steps/project/hooks.rb
@@ -36,12 +36,12 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
end
step 'I should see newly created hook' do
- expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project)
+ expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
expect(page).to have_content(@url)
end
step 'I should see newly created hook with SSL verification enabled' do
- expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project)
+ expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
expect(page).to have_content(@url)
expect(page).to have_content("SSL Verification: enabled")
end
@@ -57,7 +57,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
end
step 'hook should be triggered' do
- expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project)
+ expect(current_path).to eq namespace_project_settings_integrations_path(current_project.namespace, current_project)
expect(page).to have_selector '.flash-notice',
text: 'Hook executed successfully: HTTP 200'
end
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
index 4fda0731e2f..0a3f4649870 100644
--- a/features/steps/project/merge_requests/acceptance.rb
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -28,7 +28,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
- @project = create(:project, :public)
+ @project = create(:project, :public, :repository)
@project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb
index efbc4831ce1..3cc4fe9dafb 100644
--- a/features/steps/project/merge_requests/revert.rb
+++ b/features/steps/project/merge_requests/revert.rb
@@ -35,7 +35,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
step 'There is an open Merge Request' do
@user = create(:user)
- @project = create(:project, :public)
+ @project = create(:project, :public, :repository)
@project_member = create(:project_member, :developer, user: @user, project: @project)
@merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
end
diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb
index 1ffd5cb9de5..92936f27c20 100644
--- a/features/steps/project/redirects.rb
+++ b/features/steps/project/redirects.rb
@@ -4,11 +4,11 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
include SharedProject
step 'public project "Community"' do
- create :project, :public, name: 'Community'
+ create(:empty_project, :public, name: 'Community')
end
step 'private project "Enterprise"' do
- create :project, name: 'Enterprise'
+ create(:empty_project, :private, name: 'Enterprise')
end
step 'I visit project "Community" page' do
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index a4d29770922..772b07d0ad8 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -4,7 +4,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
include SharedPaths
step 'I visit project "Shop" services page' do
- visit namespace_project_services_path(@project.namespace, @project)
+ visit namespace_project_settings_integrations_path(@project.namespace, @project)
end
step 'I should see list of available services' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 1cc9e37b075..f18adcadcce 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -6,7 +6,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include RepoHelpers
step "I don't have write access" do
- @project = create(:project, name: "Other Project", path: "other-project")
+ @project = create(:project, :repository, name: "Other Project", path: "other-project")
@project.team << [@user, :reporter]
visit namespace_project_tree_path(@project.namespace, @project, root_ref)
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index dee6a8a5558..9183de76881 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -8,7 +8,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I own project "Delta"' do
@project = Project.find_by(name: "Delta")
- @project ||= create(:project, name: "Delta", namespace: @user.namespace)
+ @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace)
@project.team << [@user, :master]
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index c89f587f14d..6986c7ede56 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -137,7 +137,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
step 'I share project with group "OpenSource"' do
project = Project.find_by(name: 'Shop')
os_group = create(:group, name: 'OpenSource')
- create(:project, group: os_group)
+ create(:empty_project, group: os_group)
@os_user1 = create(:user)
@os_user2 = create(:user)
os_group.add_owner(@os_user1)
diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb
index fbaa408226e..ac0a1764147 100644
--- a/features/steps/shared/admin.rb
+++ b/features/steps/shared/admin.rb
@@ -2,7 +2,7 @@ module SharedAdmin
include Spinach::DSL
step 'there are projects in system' do
- 2.times { create(:project) }
+ 2.times { create(:project, :repository) }
end
step 'system has users' do
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index fe6736dacd4..de119f2c6c0 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -40,7 +40,7 @@ module SharedGroup
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, namespace: group, path: "project#{@project_count}")
+ project ||= create(:project, :repository, namespace: group, path: "project#{@project_count}")
create(:closed_issue_event, project: project)
project.team << [user, :master]
@project_count += 1
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 670e6ca49a3..718cf924729 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -256,7 +256,7 @@ module SharedPaths
end
step 'I visit project hooks page' do
- visit namespace_project_hooks_path(@project.namespace, @project)
+ visit namespace_project_settings_integrations_path(@project.namespace, @project)
end
step 'I visit project deploy keys page' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index b51152c79c6..7a6707a7dfb 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -3,19 +3,19 @@ module SharedProject
# Create a project without caring about what it's called
step "I own a project" do
- @project = create(:project, namespace: @user.namespace)
+ @project = create(:project, :repository, namespace: @user.namespace)
@project.team << [@user, :master]
end
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
- @project = create(:project, namespace: @group, public_builds: false)
+ @project = create(:project, :repository, namespace: @group, public_builds: false)
end
# Create a specific project called "Shop"
step 'I own project "Shop"' do
@project = Project.find_by(name: "Shop")
- @project ||= create(:project, name: "Shop", namespace: @user.namespace)
+ @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace)
@project.team << [@user, :master]
end
@@ -40,7 +40,7 @@ module SharedProject
# Create another specific project called "Forum"
step 'I own project "Forum"' do
@project = Project.find_by(name: "Forum")
- @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project')
+ @project ||= create(:project, :repository, name: "Forum", namespace: @user.namespace, path: 'forum_project')
@project.build_project_feature
@project.project_feature.save
@project.team << [@user, :master]
@@ -97,7 +97,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Feature Visibility")
+ expect(page).to have_content("Sharing & Permissions")
end
def current_project
@@ -121,7 +121,7 @@ module SharedProject
# ----------------------------------------
step 'archived project "Archive"' do
- create :project, :public, archived: true, name: 'Archive'
+ create(:project, :archived, :public, :repository, name: 'Archive')
end
step 'I should not see project "Archive"' do
@@ -144,7 +144,7 @@ module SharedProject
# ----------------------------------------
step 'private project "Enterprise"' do
- create :project, name: 'Enterprise'
+ create(:project, :private, :repository, name: 'Enterprise')
end
step 'I should see project "Enterprise"' do
@@ -156,7 +156,7 @@ module SharedProject
end
step 'internal project "Internal"' do
- create :project, :internal, name: 'Internal'
+ create(:project, :internal, :repository, name: 'Internal')
end
step 'I should see project "Internal"' do
@@ -168,7 +168,7 @@ module SharedProject
end
step 'public project "Community"' do
- create :project, :public, name: 'Community'
+ create(:project, :public, :repository, name: 'Community')
end
step 'I should see project "Community"' do
diff --git a/features/steps/user.rb b/features/steps/user.rb
index 59385a6ab59..271c9b097d4 100644
--- a/features/steps/user.rb
+++ b/features/steps/user.rb
@@ -38,6 +38,6 @@ class Spinach::Features::User < Spinach::FeatureSteps
end
def contributed_project
- @contributed_project ||= create(:project, :public)
+ @contributed_project ||= create(:empty_project, :public)
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 0950c3d2e88..be659fa4a6a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -129,12 +129,7 @@ module API
end
end
- # Delete all merged branches
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # DELETE /projects/:id/repository/branches/delete_merged
+ desc 'Delete all merged branches'
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 4bbdf06a49c..b6e6820c3f4 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -78,6 +78,8 @@ module API
description: params[:description]
)
+ render_validation_error!(status) if status.invalid?
+
begin
case params[:state]
when 'pending'
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 85360730841..64da7d6b86f 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -38,26 +38,25 @@ module API
present key, with: Entities::SSHKey
end
- # TODO: for 9.0 we should check if params are there with the params block
- # grape provides, at this point we'd change behaviour so we can't
- # Behaviour now if you don't provide all required params: it renders a
- # validation error or two.
desc 'Add new deploy key to currently authenticated user' do
success Entities::SSHKey
end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
post ":id/#{path}" do
- attrs = attributes_for_keys [:title, :key]
- attrs[:key].strip! if attrs[:key]
+ params[:key].strip!
# Check for an existing key joined to this project
- key = user_project.deploy_keys.find_by(key: attrs[:key])
+ key = user_project.deploy_keys.find_by(key: params[:key])
if key
present key, with: Entities::SSHKey
break
end
# Check for available deploy keys in other projects
- key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
user_project.deploy_keys << key
present key, with: Entities::SSHKey
@@ -65,7 +64,7 @@ module API
end
# Create a new deploy key
- key = DeployKey.new attrs
+ key = DeployKey.new(declared_params(include_missing: false))
if key.valid? && user_project.deploy_keys << key
present key, with: Entities::SSHKey
else
@@ -105,15 +104,19 @@ module API
present key.deploy_key, with: Entities::SSHKey
end
- desc 'Delete existing deploy key of currently authenticated user' do
+ desc 'Delete deploy key for a project' do
success Key
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id" do
- key = user_project.deploy_keys.find(params[:key_id])
- key.destroy
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ if key
+ key.destroy
+ else
+ not_found!('Deploy Key')
+ end
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 885ce7d44bc..9f59939e9ae 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -268,6 +268,13 @@ module API
end
end
+ class IssuableTimeStats < Grape::Entity
+ expose :time_estimate
+ expose :total_time_spent
+ expose :human_time_estimate
+ expose :human_total_time_spent
+ end
+
class ExternalIssue < Grape::Entity
expose :title
expose :id
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index eb2d370c68e..a1d7b323f4f 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -86,6 +86,16 @@ module API
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
+ def find_project_merge_request(id)
+ MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
+ end
+
+ def find_merge_request_with_access(id, access_level = :read_merge_request)
+ merge_request = user_project.merge_requests.find(id)
+ authorize! access_level, merge_request
+ merge_request
+ end
+
def authenticate!
unauthorized! unless current_user
end
@@ -222,7 +232,7 @@ module API
end
def render_api_error!(message, status)
- error!({ 'message' => message }, status)
+ error!({ 'message' => message }, status, header)
end
def handle_api_exception(exception)
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 161269cbd41..fe016c1ec0a 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -89,6 +89,8 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
desc 'Get a list of project issues' do
success Entities::Issue
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 07435d78468..bc3d69f6904 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -15,10 +15,8 @@ module API
end
get ":id/merge_requests/:merge_request_id/versions" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
end
@@ -34,10 +32,8 @@ module API
end
get ":id/merge_requests/:merge_request_id/versions/:version_id" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 5d1fe22f2df..7ffb38e62da 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -10,6 +10,8 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do
+ include TimeTrackingEndpoints
+
helpers do
def handle_merge_request_errors!(errors)
if errors[:project_access].any?
@@ -96,7 +98,7 @@ module API
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
delete ":id/merge_requests/:merge_request_id" do
- merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize!(:destroy_merge_request, merge_request)
merge_request.destroy
@@ -116,8 +118,8 @@ module API
success Entities::MergeRequest
end
get path do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
@@ -125,8 +127,8 @@ module API
success Entities::RepoCommit
end
get "#{path}/commits" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
present merge_request.commits, with: Entities::RepoCommit
end
@@ -134,8 +136,8 @@ module API
success Entities::MergeRequestChanges
end
get "#{path}/changes" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :read_merge_request, merge_request
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
@@ -153,8 +155,7 @@ module API
:remove_source_branch
end
put path do
- merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
- authorize! :update_merge_request, merge_request
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
@@ -180,7 +181,7 @@ module API
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
put "#{path}/merge" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
# Merge request can not be merged
# because user dont have permissions to push into target branch
@@ -216,7 +217,7 @@ module API
success Entities::MergeRequest
end
post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
@@ -233,10 +234,7 @@ module API
use :pagination
end
get "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
- authorize! :read_merge_request, merge_request
-
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
@@ -248,8 +246,7 @@ module API
requires :note, type: String, desc: 'The text of the comment'
end
post "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
- authorize! :create_note, merge_request
+ merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
opts = {
note: params[:note],
@@ -273,7 +270,7 @@ module API
use :pagination
end
get "#{path}/closes_issues" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 284e4cf549a..4d2a8f48267 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -70,21 +70,27 @@ module API
end
post ":id/#{noteables_str}/:noteable_id/notes" do
opts = {
- note: params[:body],
- noteable_type: noteables_str.classify,
- noteable_id: params[:noteable_id]
+ note: params[:body],
+ noteable_type: noteables_str.classify,
+ noteable_id: params[:noteable_id]
}
- if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
- opts[:created_at] = params[:created_at]
- end
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
+ if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
+ opts[:created_at] = params[:created_at]
+ end
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
- if note.valid?
- present note, with: Entities::const_get(note.class.name)
+ if note.valid?
+ present note, with: Entities::const_get(note.class.name)
+ else
+ not_found!("Note #{note.errors.messages}")
+ end
else
- not_found!("Note #{note.errors.messages}")
+ not_found!("Note")
end
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 3a9dfbb237c..a0abec49438 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -145,7 +145,7 @@ module API
name: :room,
type: String,
desc: 'Campfire room'
- },
+ }
],
'custom-issue-tracker' => [
{
@@ -534,7 +534,36 @@ module API
desc: 'The password of the user'
}
]
- }.freeze
+ }
+
+ service_classes = [
+ AsanaService,
+ AssemblaService,
+ BambooService,
+ BugzillaService,
+ BuildkiteService,
+ BuildsEmailService,
+ CampfireService,
+ CustomIssueTrackerService,
+ DroneCiService,
+ EmailsOnPushService,
+ ExternalWikiService,
+ FlowdockService,
+ GemnasiumService,
+ HipchatService,
+ IrkerService,
+ JiraService,
+ KubernetesService,
+ MattermostSlashCommandsService,
+ SlackSlashCommandsService,
+ PipelinesEmailService,
+ PivotaltrackerService,
+ PushoverService,
+ RedmineService,
+ SlackService,
+ MattermostService,
+ TeamcityService,
+ ].freeze
trigger_services = {
'mattermost-slash-commands' => [
@@ -568,6 +597,19 @@ module API
services.each do |service_slug, settings|
desc "Set #{service_slug} service for project"
params do
+ service_classes.each do |service|
+ event_names = service.try(:event_names) || []
+ event_names.each do |event_name|
+ services[service.to_param.tr("_", "-")] << {
+ required: false,
+ name: event_name.to_sym,
+ type: String,
+ desc: ServicesHelper.service_event_description(event_name)
+ }
+ end
+ end
+ services.freeze
+
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
@@ -581,7 +623,7 @@ module API
service_params = declared_params(include_missing: false).merge(active: true)
if service.update_attributes(service_params)
- true
+ present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
else
render_api_error!('400 Bad Request', 400)
end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 10749b34004..e11d7537cc9 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -3,8 +3,8 @@ module API
before { authenticate! }
subscribable_types = {
- 'merge_request' => proc { |id| user_project.merge_requests.find(id) },
- 'merge_requests' => proc { |id| user_project.merge_requests.find(id) },
+ 'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
+ 'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'issues' => proc { |id| find_project_issue(id) },
'labels' => proc { |id| find_project_label(id) },
}
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
new file mode 100644
index 00000000000..85b5f7d98b8
--- /dev/null
+++ b/lib/api/time_tracking_endpoints.rb
@@ -0,0 +1,114 @@
+module API
+ module TimeTrackingEndpoints
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ def issuable_name
+ declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request'
+ end
+
+ def issuable_key
+ "#{issuable_name}_id".to_sym
+ end
+
+ def update_issuable_key
+ "update_#{issuable_name}".to_sym
+ end
+
+ def read_issuable_key
+ "read_#{issuable_name}".to_sym
+ end
+
+ def load_issuable
+ @issuable ||= begin
+ case issuable_name
+ when 'issue'
+ find_project_issue(params.delete(issuable_key))
+ when 'merge_request'
+ find_project_merge_request(params.delete(issuable_key))
+ end
+ end
+ end
+
+ def update_issuable(attrs)
+ custom_params = declared_params(include_missing: false)
+ custom_params.merge!(attrs)
+
+ issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable)
+ if issuable.valid?
+ present issuable, with: Entities::IssuableTimeStats
+ else
+ render_validation_error!(issuable)
+ end
+ end
+
+ def update_service
+ issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService
+ end
+ end
+
+ issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request'
+ issuable_collection_name = issuable_name.pluralize
+ issuable_key = "#{issuable_name}_id".to_sym
+
+ desc "Set a time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)))
+ end
+
+ desc "Reset the time estimate for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(time_estimate: 0)
+ end
+
+ desc "Add spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ requires :duration, type: String, desc: 'The duration to be parsed'
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ update_issuable(spend_time: {
+ duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
+ user: current_user
+ })
+ end
+
+ desc "Reset spent time for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do
+ authorize! update_issuable_key, load_issuable
+
+ status :ok
+ update_issuable(spend_time: { duration: :reset, user: current_user })
+ end
+
+ desc "Show time stats for a project #{issuable_name}"
+ params do
+ requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}"
+ end
+ get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do
+ authorize! read_issuable_key, load_issuable
+
+ present load_issuable, with: Entities::IssuableTimeStats
+ end
+ end
+ end
+end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index ed8f48aa1e3..9bd077263a7 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -5,7 +5,7 @@ module API
before { authenticate! }
ISSUABLE_TYPES = {
- 'merge_requests' => ->(id) { user_project.merge_requests.find(id) },
+ 'merge_requests' => ->(id) { find_merge_request_with_access(id) },
'issues' => ->(id) { find_project_issue(id) }
}
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 84bfeac8041..6640168bfa2 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -20,10 +20,10 @@ module Banzai
# Examples:
#
# data_attribute(project: 1, issue: 2)
- # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
#
# data_attribute(project: 3, merge_request: 4)
- # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
#
# Returns a String
def data_attribute(attributes = {})
@@ -31,7 +31,9 @@ module Banzai
attributes[:reference_type] ||= self.class.reference_type
attributes.delete(:original) if context[:no_original_data]
- attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
+ attributes.map do |key, value|
+ %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
+ end.join(' ')
end
def escape_once(html)
@@ -51,6 +53,10 @@ module Banzai
context[:project]
end
+ def skip_project_check?
+ context[:skip_project_check]
+ end
+
def reference_class(type)
"gfm gfm-#{type} has-tooltip"
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index f842b1fb779..1aa9355b256 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -24,7 +24,7 @@ module Banzai
end
def call
- return doc if project.nil?
+ return doc if project.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
@@ -58,7 +58,7 @@ module Banzai
# have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
- if username == 'all'
+ if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
elsif namespace = namespaces[username]
link_to_namespace(namespace, link_content: link_content) || match
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index f31fb6c3f71..74663556cbb 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -52,9 +52,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
- def cacheless_render_field(object, field)
+ def cacheless_render_field(object, field, options = {})
text = object.__send__(field)
- context = object.banzai_render_context(field)
+ context = object.banzai_render_context(field).merge(options)
cacheless_render(text, context)
end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 142bce82286..c4bdef781f7 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -16,6 +16,13 @@ module Ci
not_found! unless current_runner.active?
update_runner_info
+ if current_runner.is_runner_queue_value_latest?(params[:last_update])
+ header 'X-GitLab-Last-Update', params[:last_update]
+ return build_not_found!
+ end
+
+ new_update = current_runner.ensure_runner_queue_value
+
build = Ci::RegisterBuildService.new.execute(current_runner)
if build
@@ -26,6 +33,8 @@ module Ci
else
Gitlab::Metrics.add_event(:build_not_found)
+ header 'X-GitLab-Last-Update', new_update
+
build_not_found!
end
end
diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb
index eee9a64120b..38ac6edc9f1 100644
--- a/lib/gitlab/ci/status/build/factory.rb
+++ b/lib/gitlab/ci/status/build/factory.rb
@@ -4,8 +4,11 @@ module Gitlab
module Build
class Factory < Status::Factory
def self.extended_statuses
- [Status::Build::Stop, Status::Build::Play,
- Status::Build::Cancelable, Status::Build::Retryable]
+ [[Status::Build::Cancelable,
+ Status::Build::Retryable],
+ [Status::Build::FailedAllowed,
+ Status::Build::Play,
+ Status::Build::Stop]]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
new file mode 100644
index 00000000000..807afe24bd5
--- /dev/null
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Ci
+ module Status
+ module Build
+ class FailedAllowed < SimpleDelegator
+ include Status::Extended
+
+ def label
+ 'failed (allowed to fail)'
+ end
+
+ def icon
+ 'icon_status_warning'
+ end
+
+ def group
+ 'failed_with_warnings'
+ end
+
+ def self.matches?(build, user)
+ build.failed? && build.allow_failure?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb
new file mode 100644
index 00000000000..4969a350862
--- /dev/null
+++ b/lib/gitlab/ci/status/external/common.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ module Status
+ module External
+ module Common
+ def has_details?
+ subject.target_url.present? &&
+ can?(user, :read_commit_status, subject)
+ end
+
+ def details_path
+ subject.target_url
+ end
+
+ def has_action?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb
new file mode 100644
index 00000000000..07b15bd8d97
--- /dev/null
+++ b/lib/gitlab/ci/status/external/factory.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module Ci
+ module Status
+ module External
+ class Factory < Status::Factory
+ def self.common_helpers
+ Status::External::Common
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index ae9ef895df4..15836c699c7 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -5,41 +5,46 @@ module Gitlab
def initialize(subject, user)
@subject = subject
@user = user
+ @status = subject.status || HasStatus::DEFAULT_STATUS
end
def fabricate!
- if extended_status
- extended_status.new(core_status)
- else
+ if extended_statuses.none?
core_status
+ else
+ compound_extended_status
end
end
- def self.extended_statuses
- []
+ def core_status
+ Gitlab::Ci::Status
+ .const_get(@status.capitalize)
+ .new(@subject, @user)
+ .extend(self.class.common_helpers)
end
- def self.common_helpers
- Module.new
+ def compound_extended_status
+ extended_statuses.inject(core_status) do |status, extended|
+ extended.new(status)
+ end
end
- private
+ def extended_statuses
+ return @extended_statuses if defined?(@extended_statuses)
- def simple_status
- @simple_status ||= @subject.status || :created
+ groups = self.class.extended_statuses.map do |group|
+ Array(group).find { |status| status.matches?(@subject, @user) }
+ end
+
+ @extended_statuses = groups.flatten.compact
end
- def core_status
- Gitlab::Ci::Status
- .const_get(simple_status.capitalize)
- .new(@subject, @user)
- .extend(self.class.common_helpers)
+ def self.extended_statuses
+ []
end
- def extended_status
- @extended ||= self.class.extended_statuses.find do |status|
- status.matches?(@subject, @user)
- end
+ def self.common_helpers
+ Module.new
end
end
end
diff --git a/lib/gitlab/ci/status/pipeline/factory.rb b/lib/gitlab/ci/status/pipeline/factory.rb
index 16dcb326be9..13c8343b12a 100644
--- a/lib/gitlab/ci/status/pipeline/factory.rb
+++ b/lib/gitlab/ci/status/pipeline/factory.rb
@@ -4,7 +4,7 @@ module Gitlab
module Pipeline
class Factory < Status::Factory
def self.extended_statuses
- [Pipeline::SuccessWithWarnings]
+ [Status::SuccessWarning]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb b/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
deleted file mode 100644
index 24bf8b869e0..00000000000
--- a/lib/gitlab/ci/status/pipeline/success_with_warnings.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module Gitlab
- module Ci
- module Status
- module Pipeline
- class SuccessWithWarnings < SimpleDelegator
- include Status::Extended
-
- def text
- 'passed'
- end
-
- def label
- 'passed with warnings'
- end
-
- def icon
- 'icon_status_warning'
- end
-
- def group
- 'success_with_warnings'
- end
-
- def self.matches?(pipeline, user)
- pipeline.success? && pipeline.has_warnings?
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index 689a5dd45bc..4c37f084d07 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -3,6 +3,10 @@ module Gitlab
module Status
module Stage
class Factory < Status::Factory
+ def self.extended_statuses
+ [Status::SuccessWarning]
+ end
+
def self.common_helpers
Status::Stage::Common
end
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
new file mode 100644
index 00000000000..d4cdab6957a
--- /dev/null
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Ci
+ module Status
+ ##
+ # Extended status used when pipeline or stage passed conditionally.
+ # This means that failed jobs that are allowed to fail were present.
+ #
+ class SuccessWarning < SimpleDelegator
+ include Status::Extended
+
+ def text
+ 'passed'
+ end
+
+ def label
+ 'passed with warnings'
+ end
+
+ def icon
+ 'icon_status_warning'
+ end
+
+ def group
+ 'success_with_warnings'
+ end
+
+ def self.matches?(subject, user)
+ subject.success? && subject.has_warnings?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 2ff27e46d64..4ebd48a3fc7 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -9,7 +9,9 @@ module Gitlab
end
def ensure_application_settings!
- if connect_to_db?
+ return fake_application_settings unless connect_to_db?
+
+ unless ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
begin
settings = ::ApplicationSetting.current
# In case Redis isn't running or the Redis UNIX socket file is not available
@@ -20,43 +22,23 @@ module Gitlab
settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
end
- settings || fake_application_settings
+ settings || in_memory_application_settings
end
def sidekiq_throttling_enabled?
current_application_settings.sidekiq_throttling_enabled?
end
+ def in_memory_application_settings
+ @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting::DEFAULTS)
+ # In case migrations the application_settings table is not created yet,
+ # we fallback to a simple OpenStruct
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError
+ fake_application_settings
+ end
+
def fake_application_settings
- OpenStruct.new(
- default_projects_limit: Settings.gitlab['default_projects_limit'],
- default_branch_protection: Settings.gitlab['default_branch_protection'],
- signup_enabled: Settings.gitlab['signup_enabled'],
- signin_enabled: Settings.gitlab['signin_enabled'],
- gravatar_enabled: Settings.gravatar['enabled'],
- koding_enabled: false,
- plantuml_enabled: false,
- sign_in_text: nil,
- after_sign_up_text: nil,
- help_page_text: nil,
- shared_runners_text: nil,
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- max_attachment_size: Settings.gitlab['max_attachment_size'],
- session_expire_delay: Settings.gitlab['session_expire_delay'],
- default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[gitea github bitbucket gitlab google_code fogbugz git gitlab_project],
- shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
- max_artifacts_size: Settings.artifacts['max_size'],
- require_two_factor_authentication: false,
- two_factor_grace_period: 48,
- akismet_enabled: false,
- repository_checks_enabled: true,
- container_registry_token_expire_delay: 5,
- user_default_external: false,
- sidekiq_throttling_enabled: false,
- )
+ OpenStruct.new(::ApplicationSetting::DEFAULTS)
end
private
diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index 53a148ad703..0d8791d396b 100644
--- a/lib/gitlab/cycle_analytics/base_event.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -1,13 +1,13 @@
module Gitlab
module CycleAnalytics
- class BaseEvent
- include MetricsTables
+ class BaseEventFetcher
+ include BaseQuery
- attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query
+ attr_reader :projections, :query, :stage, :order
- def initialize(project:, options:)
- @query = EventsQuery.new(project: project, options: options)
+ def initialize(project:, stage:, options:)
@project = project
+ @stage = stage
@options = options
end
@@ -19,10 +19,8 @@ module Gitlab
end.compact
end
- def custom_query(_base_query); end
-
def order
- @order || @start_time_attrs
+ @order || default_order
end
private
@@ -34,7 +32,17 @@ module Gitlab
end
def event_result
- @event_result ||= @query.execute(self).to_a
+ @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a
+ end
+
+ def events_query
+ diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
+
+ base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
+ end
+
+ def default_order
+ [@options[:start_time_attrs]].flatten.first
end
def serialize(_event)
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
new file mode 100644
index 00000000000..d560dca45c8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module CycleAnalytics
+ module BaseQuery
+ include MetricsTables
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+
+ private
+
+ def base_query
+ @base_query ||= stage_query
+ end
+
+ def stage_query
+ query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
+ join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
+ where(issue_table[:project_id].eq(@project.id)).
+ where(issue_table[:deleted_at].eq(nil)).
+ where(issue_table[:created_at].gteq(@options[:from]))
+
+ # Load merge_requests
+ query = query.join(mr_table, Arel::Nodes::OuterJoin).
+ on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
+ join(mr_metrics_table).
+ on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
+
+ query
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
new file mode 100644
index 00000000000..74bbcdcb3dd
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseStage
+ include BaseQuery
+
+ def initialize(project:, options:)
+ @project = project
+ @options = options
+ end
+
+ def events
+ event_fetcher.fetch
+ end
+
+ def as_json
+ AnalyticsStageSerializer.new.represent(self).as_json
+ end
+
+ def title
+ name.to_s.capitalize
+ end
+
+ def median
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ def name
+ raise NotImplementedError.new("Expected #{self.name} to implement name")
+ end
+
+ private
+
+ def event_fetcher
+ @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project,
+ stage: name,
+ options: event_options)
+ end
+
+ def event_options
+ @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index b394a02cc52..5245b9ca8fc 100644
--- a/lib/gitlab/cycle_analytics/review_event.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -1,22 +1,22 @@
module Gitlab
module CycleAnalytics
- class ReviewEvent < BaseEvent
+ class CodeEventFetcher < BaseEventFetcher
include MergeRequestAllowed
def initialize(*args)
- @stage = :review
- @start_time_attrs = mr_table[:created_at]
- @end_time_attrs = mr_metrics_table[:merged_at]
@projections = [mr_table[:title],
mr_table[:iid],
mr_table[:id],
mr_table[:created_at],
mr_table[:state],
mr_table[:author_id]]
+ @order = mr_table[:created_at]
super(*args)
end
+ private
+
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
end
diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb
new file mode 100644
index 00000000000..d1bc2055ba8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_stage.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_table[:created_at]
+ end
+
+ def name
+ :code
+ end
+
+ def description
+ "Time until first merge request"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb
new file mode 100644
index 00000000000..50e126cf00b
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/event_fetcher.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module EventFetcher
+ def self.[](stage_name)
+ CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb
deleted file mode 100644
index 2d703d76cbb..00000000000
--- a/lib/gitlab/cycle_analytics/events.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class Events
- def initialize(project:, options:)
- @project = project
- @options = options
- end
-
- def issue_events
- IssueEvent.new(project: @project, options: @options).fetch
- end
-
- def plan_events
- PlanEvent.new(project: @project, options: @options).fetch
- end
-
- def code_events
- CodeEvent.new(project: @project, options: @options).fetch
- end
-
- def test_events
- TestEvent.new(project: @project, options: @options).fetch
- end
-
- def review_events
- ReviewEvent.new(project: @project, options: @options).fetch
- end
-
- def staging_events
- StagingEvent.new(project: @project, options: @options).fetch
- end
-
- def production_events
- ProductionEvent.new(project: @project, options: @options).fetch
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb
deleted file mode 100644
index 2418832ccc2..00000000000
--- a/lib/gitlab/cycle_analytics/events_query.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class EventsQuery
- attr_reader :project
-
- def initialize(project:, options: {})
- @project = project
- @from = options[:from]
- @branch = options[:branch]
- @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch)
- end
-
- def execute(stage_class)
- @stage_class = stage_class
-
- ActiveRecord::Base.connection.exec_query(query.to_sql)
- end
-
- private
-
- def query
- base_query = @fetcher.base_query_for(@stage_class.stage)
- diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs)
-
- @stage_class.custom_query(base_query)
-
- base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc)
- end
-
- def extract_epoch(arel_attribute)
- return arel_attribute unless Gitlab::Database.postgresql?
-
- Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))})
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb
deleted file mode 100644
index 705b7e5ce24..00000000000
--- a/lib/gitlab/cycle_analytics/issue_event.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class IssueEvent < BaseEvent
- include IssueAllowed
-
- def initialize(*args)
- @stage = :issue
- @start_time_attrs = issue_table[:created_at]
- @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at],
- issue_metrics_table[:first_added_to_board_at]]
- @projections = [issue_table[:title],
- issue_table[:iid],
- issue_table[:id],
- issue_table[:created_at],
- issue_table[:author_id]]
-
- super(*args)
- end
-
- private
-
- def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 4868c3c6237..0d8da99455e 100644
--- a/lib/gitlab/cycle_analytics/production_event.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -1,12 +1,9 @@
module Gitlab
module CycleAnalytics
- class ProductionEvent < BaseEvent
+ class IssueEventFetcher < BaseEventFetcher
include IssueAllowed
def initialize(*args)
- @stage = :production
- @start_time_attrs = issue_table[:created_at]
- @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
@projections = [issue_table[:title],
issue_table[:iid],
issue_table[:id],
diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb
new file mode 100644
index 00000000000..d2068fbc38f
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class IssueStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= issue_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]]
+ end
+
+ def name
+ :issue
+ end
+
+ def description
+ "Time before an issue gets scheduled"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
deleted file mode 100644
index b71e8735e27..00000000000
--- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class MetricsFetcher
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
- include MetricsTables
-
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
- def initialize(project:, from:, branch:)
- @project = project
- @project = project
- @from = from
- @branch = branch
- end
-
- def calculate_metric(name, start_time_attrs, end_time_attrs)
- cte_table = Arel::Table.new("cte_table_for_#{name}")
-
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s))
-
- median_datetime(cte_table, interval_query, name)
- end
-
- # Join table with a row for every <issue,merge_request> pair (where the merge request
- # closes the given issue) with issue and merge request metrics included. The metrics
- # are loaded with an inner join, so issues / merge requests without metrics are
- # automatically excluded.
- def base_query_for(name)
- # Load issues
- query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
- join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
- where(issue_table[:project_id].eq(@project.id)).
- where(issue_table[:deleted_at].eq(nil)).
- where(issue_table[:created_at].gteq(@from))
-
- query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
-
- # Load merge_requests
- query = query.join(mr_table, Arel::Nodes::OuterJoin).
- on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
- join(mr_metrics_table).
- on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
- end
-
- query
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 7c3f0e9989f..88a8710dbe6 100644
--- a/lib/gitlab/cycle_analytics/plan_event.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -1,19 +1,17 @@
module Gitlab
module CycleAnalytics
- class PlanEvent < BaseEvent
+ class PlanEventFetcher < BaseEventFetcher
def initialize(*args)
- @stage = :plan
- @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at]
- @end_time_attrs = [issue_metrics_table[:first_added_to_board_at],
- issue_metrics_table[:first_mentioned_in_commit_at]]
@projections = [mr_diff_table[:st_commits].as('commits'),
issue_metrics_table[:first_mentioned_in_commit_at]]
super(*args)
end
- def custom_query(base_query)
+ def events_query
base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
+
+ super
end
private
diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb
new file mode 100644
index 00000000000..3b4dfc6a30e
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/plan_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class PlanStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
+ issue_metrics_table[:first_added_to_board_at]]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
+ end
+
+ def name
+ :plan
+ end
+
+ def description
+ "Time before an issue starts implementation"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
new file mode 100644
index 00000000000..0fa2e87f673
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionEventFetcher < IssueEventFetcher
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
new file mode 100644
index 00000000000..d693443bfa4
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module ProductionHelper
+ def stage_query
+ super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from]))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb
new file mode 100644
index 00000000000..2a6bcc80116
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_stage.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionStage < BaseStage
+ include ProductionHelper
+
+ def start_time_attrs
+ @start_time_attrs ||= issue_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ def name
+ :production
+ end
+
+ def description
+ "From issue creation until deploy to production"
+ end
+
+ def query
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index 2afdf0b8518..4df0bd06393 100644
--- a/lib/gitlab/cycle_analytics/code_event.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -1,25 +1,19 @@
module Gitlab
module CycleAnalytics
- class CodeEvent < BaseEvent
+ class ReviewEventFetcher < BaseEventFetcher
include MergeRequestAllowed
def initialize(*args)
- @stage = :code
- @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at]
- @end_time_attrs = mr_table[:created_at]
@projections = [mr_table[:title],
mr_table[:iid],
mr_table[:id],
mr_table[:created_at],
mr_table[:state],
mr_table[:author_id]]
- @order = mr_table[:created_at]
super(*args)
end
- private
-
def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
end
diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb
new file mode 100644
index 00000000000..fbaa3010d81
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/review_stage.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ class ReviewStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= mr_table[:created_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:merged_at]
+ end
+
+ def name
+ :review
+ end
+
+ def description
+ "Time between merge request creation and merge/close"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb
new file mode 100644
index 00000000000..28e0455df59
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/stage.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module Stage
+ def self.[](stage_name)
+ CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
new file mode 100644
index 00000000000..b34baf5b081
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module CycleAnalytics
+ class StageSummary
+ def initialize(project, from:, current_user:)
+ @project = project
+ @from = from
+ @current_user = current_user
+ end
+
+ def data
+ [serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)),
+ serialize(Summary::Commit.new(project: @project, from: @from)),
+ serialize(Summary::Deploy.new(project: @project, from: @from))]
+ end
+
+ private
+
+ def serialize(summary_object)
+ AnalyticsSummarySerializer.new.represent(summary_object).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
index a1f30b716f6..a34731a5fcd 100644
--- a/lib/gitlab/cycle_analytics/staging_event.rb
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -1,10 +1,7 @@
module Gitlab
module CycleAnalytics
- class StagingEvent < BaseEvent
+ class StagingEventFetcher < BaseEventFetcher
def initialize(*args)
- @stage = :staging
- @start_time_attrs = mr_metrics_table[:merged_at]
- @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
@projections = [build_table[:id]]
@order = build_table[:created_at]
@@ -17,8 +14,10 @@ module Gitlab
super
end
- def custom_query(base_query)
+ def events_query
base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
+
+ super
end
private
diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb
new file mode 100644
index 00000000000..945909a4d62
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/staging_stage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module CycleAnalytics
+ class StagingStage < BaseStage
+ include ProductionHelper
+ def start_time_attrs
+ @start_time_attrs ||= mr_metrics_table[:merged_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
+ end
+
+ def name
+ :staging
+ end
+
+ def description
+ "From merge request merge until deploy to production"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb
new file mode 100644
index 00000000000..43fa3795e5c
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/base.rb
@@ -0,0 +1,20 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Base
+ def initialize(project:, from:)
+ @project = project
+ @from = from
+ end
+
+ def title
+ self.class.name.demodulize
+ end
+
+ def value
+ raise NotImplementedError.new("Expected #{self.name} to implement value")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
new file mode 100644
index 00000000000..7b8faa4d854
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Commit < Base
+ def value
+ @value ||= count_commits
+ end
+
+ private
+
+ # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
+ # a limit. Since we need a commit count, we _can't_ enforce a limit, so
+ # the easiest way forward is to replicate the relevant portions of the
+ # `log` function here.
+ def count_commits
+ return unless ref
+
+ repository = @project.repository.raw_repository
+ sha = @project.repository.commit(ref).sha
+
+ cmd = %W(git --git-dir=#{repository.path} log)
+ cmd << '--format=%H'
+ cmd << "--after=#{@from.iso8601}"
+ cmd << sha
+
+ output, status = Gitlab::Popen.popen(cmd)
+
+ raise IOError, output unless status.zero?
+
+ output.lines.count
+ end
+
+ def ref
+ @ref ||= @project.default_branch.presence
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
new file mode 100644
index 00000000000..06032e9200e
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -0,0 +1,11 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Deploy < Base
+ def value
+ @value ||= @project.deployments.where("created_at > ?", @from).count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb
new file mode 100644
index 00000000000..008468f24b9
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/issue.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Issue < Base
+ def initialize(project:, from:, current_user:)
+ @project = project
+ @from = from
+ @current_user = current_user
+ end
+
+ def title
+ 'New Issue'
+ end
+
+ def value
+ @value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb
deleted file mode 100644
index d553d0b5aec..00000000000
--- a/lib/gitlab/cycle_analytics/test_event.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module CycleAnalytics
- class TestEvent < StagingEvent
- def initialize(*args)
- super(*args)
-
- @stage = :test
- @start_time_attrs = mr_metrics_table[:latest_build_started_at]
- @end_time_attrs = mr_metrics_table[:latest_build_finished_at]
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
new file mode 100644
index 00000000000..a2589c6601a
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb
@@ -0,0 +1,6 @@
+module Gitlab
+ module CycleAnalytics
+ class TestEventFetcher < StagingEventFetcher
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
new file mode 100644
index 00000000000..0079d56e0e4
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ module CycleAnalytics
+ class TestStage < BaseStage
+ def start_time_attrs
+ @start_time_attrs ||= mr_metrics_table[:latest_build_started_at]
+ end
+
+ def end_time_attrs
+ @end_time_attrs ||= mr_metrics_table[:latest_build_finished_at]
+ end
+
+ def name
+ :test
+ end
+
+ def description
+ "Total test time for all commits/merges"
+ end
+
+ def stage_query
+ if @options[:branch]
+ super.where(build_table[:ref].eq(@options[:branch]))
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 1444d25ebc7..08607c27c09 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -103,6 +103,11 @@ module Gitlab
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end
+ def extract_diff_epoch(diff)
+ return diff unless Gitlab::Database.postgresql?
+
+ Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))})
+ end
# Need to cast '0' to an INTERVAL before we can check if the interval is positive
def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd3267e2a80..bd2f5d3615e 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -1,10 +1,11 @@
require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
+require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab
module Email
module Handler
- HANDLERS = [CreateNoteHandler, CreateIssueHandler]
+ HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler]
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
index 7cccf465334..3f6ace0311a 100644
--- a/lib/gitlab/email/handler/base_handler.rb
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -9,52 +9,13 @@ module Gitlab
@mail_key = mail_key
end
- def message
- @message ||= process_message
- end
-
- def author
+ def can_execute?
raise NotImplementedError
end
- def project
+ def execute
raise NotImplementedError
end
-
- private
-
- 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)
- end
-
- def process_message
- message = ReplyParser.new(mail).execute.strip
- add_attachments(message)
- end
-
- def add_attachments(reply)
- attachments = Email::AttachmentUploader.new(mail).execute(project)
-
- reply + attachments.map do |link|
- "\n\n#{link[:markdown]}"
- end.join
- end
-
- def verify_record!(record:, invalid_exception:, record_name:)
- return if record.persisted?
- return if record.errors.key?(:commands_only)
-
- error_title = "The #{record_name} could not be created for the following reasons:"
-
- msg = error_title + record.errors.full_messages.map do |error|
- "\n\n- #{error}"
- end.join
-
- raise invalid_exception, msg
- end
end
end
end
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 9f90a3ec2b2..127fae159d5 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -5,6 +5,7 @@ module Gitlab
module Email
module Handler
class CreateIssueHandler < BaseHandler
+ include ReplyProcessing
attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key)
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
index 447c7a6a6b9..d87ba427f4b 100644
--- a/lib/gitlab/email/handler/create_note_handler.rb
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -1,10 +1,13 @@
require 'gitlab/email/handler/base_handler'
+require 'gitlab/email/handler/reply_processing'
module Gitlab
module Email
module Handler
class CreateNoteHandler < BaseHandler
+ include ReplyProcessing
+
def can_handle?
mail_key =~ /\A\w+\z/
end
@@ -24,6 +27,8 @@ module Gitlab
record_name: 'comment')
end
+ private
+
def author
sent_notification.recipient
end
@@ -36,8 +41,6 @@ module Gitlab
@sent_notification ||= SentNotification.for(mail_key)
end
- private
-
def create_note
Notes::CreateService.new(
project,
diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb
new file mode 100644
index 00000000000..32c5caf93e8
--- /dev/null
+++ b/lib/gitlab/email/handler/reply_processing.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module Email
+ module Handler
+ module ReplyProcessing
+ private
+
+ def author
+ raise NotImplementedError
+ end
+
+ def project
+ raise NotImplementedError
+ end
+
+ def message
+ @message ||= process_message
+ end
+
+ def process_message
+ message = ReplyParser.new(mail).execute.strip
+ add_attachments(message)
+ end
+
+ def add_attachments(reply)
+ attachments = Email::AttachmentUploader.new(mail).execute(project)
+
+ reply + attachments.map do |link|
+ "\n\n#{link[:markdown]}"
+ end.join
+ end
+
+ 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)
+ end
+
+ def verify_record!(record:, invalid_exception:, record_name:)
+ return if record.persisted?
+ return if record.errors.key?(:commands_only)
+
+ error_title = "The #{record_name} could not be created for the following reasons:"
+
+ msg = error_title + record.errors.full_messages.map do |error|
+ "\n\n- #{error}"
+ end.join
+
+ raise invalid_exception, msg
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb
new file mode 100644
index 00000000000..97d7a8d65ff
--- /dev/null
+++ b/lib/gitlab/email/handler/unsubscribe_handler.rb
@@ -0,0 +1,32 @@
+require 'gitlab/email/handler/base_handler'
+
+module Gitlab
+ module Email
+ module Handler
+ class UnsubscribeHandler < BaseHandler
+ def can_handle?
+ mail_key =~ /\A\w+#{Regexp.escape(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX)}\z/
+ end
+
+ def execute
+ raise SentNotificationNotFoundError unless sent_notification
+ return unless sent_notification.unsubscribable?
+
+ noteable = sent_notification.noteable
+ raise NoteableNotFoundError unless noteable
+ noteable.unsubscribe(sent_notification.recipient)
+ end
+
+ private
+
+ def sent_notification
+ @sent_notification ||= SentNotification.for(reply_key)
+ end
+
+ def reply_key
+ mail_key.sub(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX, '')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 3f635be22ba..a55adc9b1c8 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class ProjectCreator
+ include Gitlab::CurrentSettings
+
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data, type: 'github')
@@ -34,7 +36,7 @@ module Gitlab
end
def visibility_level
- repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
end
#
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index e6ecd118609..08ad3274b38 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -6,6 +6,7 @@ project_tree:
- :events
- issues:
- :events
+ - :timelogs
- notes:
- :author
- :events
@@ -27,6 +28,7 @@ project_tree:
- :events
- :merge_request_diff
- :events
+ - :timelogs
- label_links:
- label:
:priorities
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index b790733f4a7..2405b94db50 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -1,13 +1,10 @@
module Gitlab
module ImportExport
class MembersMapper
- attr_reader :missing_author_ids
-
def initialize(exported_members:, user:, project:)
- @exported_members = exported_members
+ @exported_members = user.admin? ? exported_members : []
@user = user
@project = project
- @missing_author_ids = []
# This needs to run first, as second call would be from #map
# which means project members already exist.
@@ -39,7 +36,6 @@ module Gitlab
def missing_keys_tracking_hash
Hash.new do |_, key|
- @missing_author_ids << key
default_user_id
end
end
@@ -64,7 +60,7 @@ module Gitlab
end
def find_project_user_query(member)
- user_arel[:username].eq(member['user']['username']).or(user_arel[:email].eq(member['user']['email']))
+ user_arel[:email].eq(member['user']['email']).or(user_arel[:username].eq(member['user']['username']))
end
def user_arel
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 7a649f28340..19e43cce768 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -14,7 +14,7 @@ module Gitlab
priorities: :label_priorities,
label: :project_label }.freeze
- USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id].freeze
+ USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
@@ -80,17 +80,13 @@ module Gitlab
# is left.
def set_note_author
old_author_id = @relation_hash['author_id']
-
- # Users with admin access can map users
- @relation_hash['author_id'] = admin_user? ? @members_mapper.map[old_author_id] : @members_mapper.default_user_id
-
author = @relation_hash.delete('author')
- update_note_for_missing_author(author['name']) if missing_author?(old_author_id)
+ update_note_for_missing_author(author['name']) unless has_author?(old_author_id)
end
- def missing_author?(old_author_id)
- !admin_user? || @members_mapper.missing_author_ids.include?(old_author_id)
+ def has_author?(old_author_id)
+ admin_user? && @members_mapper.map.keys.include?(old_author_id)
end
def missing_author_note(updated_at, author_name)
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 45958710c13..52276cbcd9a 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -5,8 +5,6 @@
#
module Gitlab
module ImportSources
- extend CurrentSettings
-
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 801dfde9a36..b91012d6405 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,5 +1,6 @@
module Gitlab
module IncomingEmail
+ UNSUBSCRIBE_SUFFIX = '+unsubscribe'.freeze
WILDCARD_PLACEHOLDER = '%{key}'.freeze
class << self
@@ -18,7 +19,11 @@ module Gitlab
end
def reply_address(key)
- config.address.gsub(WILDCARD_PLACEHOLDER, key)
+ config.address.sub(WILDCARD_PLACEHOLDER, key)
+ end
+
+ def unsubscribe_address(key)
+ config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end
def key_from_address(address)
@@ -49,7 +54,7 @@ module Gitlab
return nil unless wildcard_address
regex = Regexp.escape(wildcard_address)
- regex = regex.gsub(Regexp.escape('%{key}'), "(.+)")
+ regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
Regexp.new(regex).freeze
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 6bdf3db9cb8..db325c00705 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -71,6 +71,14 @@ module Gitlab
)
end
+ def single_commit_result?
+ commits_count == 1 && total_result_count == 1
+ end
+
+ def total_result_count
+ issues_count + merge_requests_count + milestones_count + notes_count + blobs_count + wiki_blobs_count + commits_count
+ end
+
private
def blobs
@@ -114,7 +122,25 @@ module Gitlab
end
def commits
- @commits ||= project.repository.find_commits_by_message(query)
+ @commits ||= find_commits(query)
+ end
+
+ def find_commits(query)
+ return [] unless Ability.allowed?(@current_user, :download_code, @project)
+
+ commits = find_commits_by_message(query)
+ commit_by_sha = find_commit_by_sha(query)
+ commits |= [commit_by_sha] if commit_by_sha
+ commits
+ end
+
+ def find_commits_by_message(query)
+ project.repository.find_commits_by_message(query)
+ end
+
+ def find_commit_by_sha(query)
+ key = query.strip
+ project.repository.commit(key) if Commit.valid_hash?(key)
end
def project_ids_relation
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 35212992698..c9c65f76f4b 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -43,6 +43,10 @@ module Gitlab
@milestones_count ||= milestones.count
end
+ def single_commit_result?
+ false
+ end
+
private
def projects
diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb
new file mode 100644
index 00000000000..d615c24149a
--- /dev/null
+++ b/lib/gitlab/time_tracking_formatter.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module TimeTrackingFormatter
+ extend self
+
+ def parse(string)
+ with_custom_config do
+ string.sub!(/\A-/, '')
+
+ seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil
+ seconds *= -1 if seconds && Regexp.last_match
+ seconds
+ end
+ end
+
+ def output(seconds)
+ with_custom_config do
+ ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil
+ end
+ end
+
+ def with_custom_config
+ # We may want to configure it through project settings in a future version.
+ ChronicDuration.hours_per_day = 8
+ ChronicDuration.days_per_week = 5
+
+ result = yield
+
+ ChronicDuration.hours_per_day = 24
+ ChronicDuration.days_per_week = 7
+
+ result
+ end
+ end
+end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 6c7e673fb9f..6ce9b229294 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -35,7 +35,9 @@ module Gitlab
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
- access_levels.any? { |access_level| access_level.check_access(user) }
+ has_access = access_levels.any? { |access_level| access_level.check_access(user) }
+
+ has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
else
user.can?(:push_code, project)
end
diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb
new file mode 100644
index 00000000000..83c8ba5c1cf
--- /dev/null
+++ b/lib/gitlab/view/presenter/base.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module View
+ module Presenter
+ module Base
+ extend ActiveSupport::Concern
+
+ include Gitlab::Routing
+ include Gitlab::Allowable
+
+ attr_reader :subject
+
+ def can?(user, action, overriden_subject = nil)
+ super(user, action, overriden_subject || subject)
+ end
+
+ class_methods do
+ def presenter?
+ true
+ end
+
+ def presents(name)
+ define_method(name) { subject }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/view/presenter/delegated.rb b/lib/gitlab/view/presenter/delegated.rb
new file mode 100644
index 00000000000..f4d330c590e
--- /dev/null
+++ b/lib/gitlab/view/presenter/delegated.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module View
+ module Presenter
+ class Delegated < SimpleDelegator
+ include Gitlab::View::Presenter::Base
+
+ def initialize(subject, **attributes)
+ @subject = subject
+
+ attributes.each do |key, value|
+ define_singleton_method(key) { value }
+ end
+
+ super(subject)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/view/presenter/factory.rb b/lib/gitlab/view/presenter/factory.rb
new file mode 100644
index 00000000000..d172d61e2c9
--- /dev/null
+++ b/lib/gitlab/view/presenter/factory.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module View
+ module Presenter
+ class Factory
+ def initialize(subject, **attributes)
+ @subject = subject
+ @attributes = attributes
+ end
+
+ def fabricate!
+ presenter_class.new(subject, attributes)
+ end
+
+ private
+
+ attr_reader :subject, :attributes
+
+ def presenter_class
+ "#{subject.class.name}Presenter".constantize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/view/presenter/simple.rb b/lib/gitlab/view/presenter/simple.rb
new file mode 100644
index 00000000000..b7653a0f3cc
--- /dev/null
+++ b/lib/gitlab/view/presenter/simple.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module View
+ module Presenter
+ class Simple
+ include Gitlab::View::Presenter::Base
+
+ def initialize(subject, **attributes)
+ @subject = subject
+
+ attributes.each do |key, value|
+ define_singleton_method(key) { value }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index d28bb583fe7..a3b502ffd6a 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -15,10 +15,17 @@ module Gitlab
class << self
def git_http_ok(repository, user)
- {
+ params = {
GL_ID: Gitlab::GlId.gl_id(user),
RepoPath: repository.path_to_repo,
}
+
+ params.merge!(
+ GitalySocketPath: Gitlab.config.gitaly.socket_path,
+ GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
+ ) if Gitlab.config.gitaly.socket_path.present?
+
+ params
end
def lfs_upload_ok(oid, size)
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
index ec2903b7ec6..e55c0d6ac49 100644
--- a/lib/mattermost/client.rb
+++ b/lib/mattermost/client.rb
@@ -8,21 +8,31 @@ module Mattermost
@user = user
end
- private
-
def with_session(&blk)
Mattermost::Session.new(user).with_session(&blk)
end
- def json_get(path, options = {})
+ private
+
+ # Should be used in a session manually
+ def get(session, path, options = {})
+ json_response session.get(path, options)
+ end
+
+ # Should be used in a session manually
+ def post(session, path, options = {})
+ json_response session.post(path, options)
+ end
+
+ def session_get(path, options = {})
with_session do |session|
- json_response session.get(path, options)
+ get(session, path, options)
end
end
- def json_post(path, options = {})
+ def session_post(path, options = {})
with_session do |session|
- json_response session.post(path, options)
+ post(session, path, options)
end
end
diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb
index d1e4bb0eccf..33e450d7f0a 100644
--- a/lib/mattermost/command.rb
+++ b/lib/mattermost/command.rb
@@ -1,7 +1,7 @@
module Mattermost
class Command < Client
def create(params)
- response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create",
+ response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create",
body: params.to_json)
response['token']
diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb
index 784eca6ab5a..09dfd082b3a 100644
--- a/lib/mattermost/team.rb
+++ b/lib/mattermost/team.rb
@@ -1,7 +1,7 @@
module Mattermost
class Team < Client
def all
- json_get('/api/v3/teams/all')
+ session_get('/api/v3/teams/all')
end
end
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index dffea8ed155..f7c831892ee 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -11,8 +11,10 @@ namespace :gitlab do
gem_version = run_command(%W(gem --version))
# check Bundler version
bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
- # check Bundler version
+ # check Rake version
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+ # check redis version
+ redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
puts ""
puts "System information".color(:yellow)
@@ -24,6 +26,7 @@ namespace :gitlab do
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
+ puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
diff --git a/public/robots.txt b/public/robots.txt
index 7d69fad59d1..123272a9834 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -4,13 +4,12 @@
# User-Agent: *
# Disallow: /
-User-Agent: *
-
# Add a 1 second delay between successive requests to the same server, limits resources used by crawler
# Only some crawlers respect this setting, e.g. Googlebot does not
# Crawl-delay: 1
# Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application
+User-Agent: *
Disallow: /autocomplete/users
Disallow: /search
Disallow: /api
@@ -23,12 +22,14 @@ Disallow: /groups/*/edit
Disallow: /users
# Global snippets
+User-Agent: *
Disallow: /s/
Disallow: /snippets/new
Disallow: /snippets/*/edit
Disallow: /snippets/*/raw
# Project details
+User-Agent: *
Disallow: /*/*.git
Disallow: /*/*/fork/new
Disallow: /*/*/repository/archive*
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
new file mode 100644
index 00000000000..e5cdd52307e
--- /dev/null
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Admin::ServicesController do
+ let(:admin) { create(:admin) }
+
+ before { sign_in(admin) }
+
+ describe 'GET #edit' do
+ let!(:project) { create(:empty_project) }
+
+ Service.available_services_names.each do |service_name|
+ context "#{service_name}" do
+ let!(:service) do
+ service_template = service_name.concat("_service").camelize.constantize
+ service_template.where(template: true).first_or_create
+ end
+
+ it 'successfully displays the template' do
+ get :edit, id: service.id
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 56ecf2bb644..cfe18dd4b6c 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
describe HealthCheckController do
+ include StubENV
+
let(:token) { current_application_settings.health_check_access_token }
let(:json_response) { JSON.parse(response.body) }
let(:xml_response) { Hash.from_xml(response.body)['hash'] }
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
describe 'GET #index' do
context 'when services are up but NO access token' do
it 'returns a not found page' do
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 646b097d74e..0fa06a38d2a 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -1,9 +1,10 @@
require 'spec_helper'
describe Projects::CommitController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:commit) { project.commit("master") }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:commit) { project.commit("master") }
+ let(:pipeline) { create(:ci_pipeline, project: project, commit: commit) }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
@@ -309,4 +310,35 @@ describe Projects::CommitController do
end
end
end
+
+ describe 'GET pipelines' do
+ def get_pipelines(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ }
+
+ get :pipelines, params.merge(extra_params)
+ end
+
+ context 'when the commit exists' do
+ context 'when the commit has one or more pipelines' do
+ it 'shows pipelines' do
+ get_pipelines(id: commit.id)
+
+ expect(response).to be_ok
+ end
+ end
+ end
+
+ context 'when the commit does not exist' do
+ before do
+ get_pipelines(id: 'e7a412c8da9f6d0081a633a4a402dde1c4694ebd')
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 7a57801c437..b03c4b52de6 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -64,6 +64,36 @@ describe Projects::CompareController do
expect(assigns(:diffs)).to eq(nil)
expect(assigns(:commits)).to eq(nil)
end
+
+ it 'redirects back to index when params[:from] is empty and preserves params[:to]' do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: '',
+ to: 'master')
+
+ expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, to: 'master'))
+ end
+
+ it 'redirects back to index when params[:to] is empty and preserves params[:from]' do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: 'master',
+ to: '')
+
+ expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, from: 'master'))
+ end
+
+ it 'redirects back to index when params[:from] and params[:to] are empty' do
+ post(:create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: '',
+ to: '')
+
+ expect(response).to redirect_to(namespace_project_compare_index_path)
+ end
end
describe 'GET diff_for_path' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index e2321f2034b..b5987a83df0 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -326,6 +326,20 @@ describe Projects::IssuesController do
end
describe 'POST #create' do
+ def post_new_issue(attrs = {})
+ sign_in(user)
+ project = create(:empty_project, :public)
+ project.team << [user, :developer]
+
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ issue: { title: 'Title', description: 'Description' }.merge(attrs)
+ }
+
+ project.issues.first
+ end
+
context 'resolving discussions in MergeRequest' do
let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
let(:merge_request) { discussion.noteable }
@@ -369,13 +383,7 @@ describe Projects::IssuesController do
end
def post_spam_issue
- sign_in(user)
- spam_project = create(:empty_project, :public)
- post :create, {
- namespace_id: spam_project.namespace.to_param,
- project_id: spam_project.to_param,
- issue: { title: 'Spam Title', description: 'Spam lives here' }
- }
+ post_new_issue(title: 'Spam Title', description: 'Spam lives here')
end
it 'rejects an issue recognized as spam' do
@@ -396,18 +404,26 @@ describe Projects::IssuesController do
request.env['action_dispatch.remote_ip'] = '127.0.0.1'
end
- def post_new_issue
+ it 'creates a user agent detail' do
+ expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ end
+ end
+
+ context 'when description has slash commands' do
+ before do
sign_in(user)
- project = create(:empty_project, :public)
- post :create, {
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- issue: { title: 'Title', description: 'Description' }
- }
end
- it 'creates a user agent detail' do
- expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ it 'can add spent time' do
+ issue = post_new_issue(description: '/spend 1h')
+
+ expect(issue.total_time_spent).to eq(3600)
+ end
+
+ it 'can set the time estimate' do
+ issue = post_new_issue(description: '/estimate 2h')
+
+ expect(issue.time_estimate).to eq(7200)
end
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 2e44b5128b4..a6e708c01e4 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -54,6 +54,7 @@ describe Projects::ServicesController do
context 'on successful update' do
it 'sets the flash' do
expect(service).to receive(:to_param).and_return('hipchat')
+ expect(service).to receive(:event_names).and_return(HipchatService.event_names)
put :update,
namespace_id: project.namespace.id,
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
new file mode 100644
index 00000000000..e0f9a5b24a6
--- /dev/null
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Projects::Settings::IntegrationsController do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index ee3b17b8bf1..7f557b25ccb 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -3,11 +3,12 @@ FactoryGirl.define do
transient do
name 'test'
status nil
+ warnings nil
pipeline factory: :ci_empty_pipeline
end
initialize_with do
- Ci::Stage.new(pipeline, name: name, status: status)
+ Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings)
end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index a10ba629760..476f2a37793 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -13,6 +13,7 @@ FactoryGirl.define do
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_project_snippet, traits: [:on_project_snippet]
+ factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :system_note, traits: [:system]
factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
@@ -70,6 +71,11 @@ FactoryGirl.define do
noteable { create(:project_snippet, project: project) }
end
+ trait :on_personal_snippet do
+ noteable { create(:personal_snippet) }
+ project nil
+ end
+
trait :system do
system true
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1cdbe4fc9a5..992580a6b34 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -32,6 +32,10 @@ FactoryGirl.define do
request_access_enabled true
end
+ trait :repository do
+ # no-op... for now!
+ end
+
trait :empty_repo do
after(:create) do |project|
project.create_repository
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
new file mode 100644
index 00000000000..12fc4ec4486
--- /dev/null
+++ b/spec/factories/timelogs.rb
@@ -0,0 +1,9 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :timelog do
+ time_spent 3600
+ user
+ association :trackable, factory: :issue
+ end
+end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 082b02116c0..85a8c263643 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -28,6 +28,10 @@ FactoryGirl.define do
action { Todo::APPROVAL_REQUIRED }
end
+ trait :unmergeable do
+ action { Todo::UNMERGEABLE }
+ end
+
trait :done do
state :done
end
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 66044b44495..e8e080ce3e2 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -1,10 +1,13 @@
require 'rails_helper'
feature 'Admin disables Git access protocol', feature: true do
+ include StubENV
+
let(:project) { create(:empty_project, :empty_repo) }
let(:admin) { create(:admin) }
background do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as(admin)
end
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index dec2dedf2b5..f7e49a56deb 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
feature "Admin Health Check", feature: true do
+ include StubENV
include WaitForAjax
before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
@@ -12,11 +14,12 @@ feature "Admin Health Check", feature: true do
visit admin_health_check_path
end
- it { page.has_text? 'Health Check' }
- it { page.has_text? 'Health information can be retrieved' }
-
it 'has a health check access token' do
+ page.has_text? 'Health Check'
+ page.has_text? 'Health information can be retrieved'
+
token = current_application_settings.health_check_access_token
+
expect(page).to have_content("Access token is #{token}")
expect(page).to have_selector('#health-check-token', text: token)
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index a5b88812b75..87a8f62687a 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -10,7 +10,7 @@ describe "Admin::Projects", feature: true do
end
describe "GET /admin/projects" do
- let!(:archived_project) { create :project, :public, archived: true }
+ let!(:archived_project) { create :project, :public, :archived }
before do
visit admin_projects_path
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index d92c66b689d..f05fbe3d062 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -1,7 +1,10 @@
require 'spec_helper'
describe "Admin Runners" do
+ include StubENV
+
before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 47fa2f14307..de42ab81fac 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -1,7 +1,10 @@
require 'spec_helper'
feature 'Admin updates settings', feature: true do
- before(:each) do
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
login_as :admin
visit admin_application_settings_path
end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 661fb761809..855247de2ea 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -1,7 +1,12 @@
require 'rails_helper'
feature 'Admin uses repository checks', feature: true do
- before { login_as :admin }
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ login_as :admin
+ end
scenario 'to trigger a single check' do
project = create(:empty_project)
@@ -29,7 +34,7 @@ feature 'Admin uses repository checks', feature: true do
scenario 'to clear all repository checks', js: true do
visit admin_application_settings_path
-
+
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
click_link 'Clear all repository checks'
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 30b80aa82b0..78a11ffee99 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do
include_examples 'project features apply to issuables', MergeRequest
context 'archived issuable' do
- let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) }
+ let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) }
let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
let(:access_level) { ProjectFeature::ENABLED }
let(:user) { user_in_group }
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 6f6a2532c04..8a155c3bfc5 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -43,6 +43,14 @@ describe 'Dropdown assignee', js: true, feature: true do
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
+ it 'shows assigned to me link' do
+ filtered_search.set('assignee:')
+
+ page.within js_dropdown_assignee do
+ expect(page).to have_content('Assigned to me')
+ end
+ end
+
it 'closes when the search bar is unfocused' do
find('body').click()
@@ -66,6 +74,12 @@ describe 'Dropdown assignee', js: true, feature: true do
expect(dropdown_assignee_size).to eq(3)
end
+
+ it 'shows current user at top of dropdown' do
+ send_keys_to_filtered_search('assignee:')
+
+ expect(first('#js-dropdown-assignee .filter-dropdown li')).to have_content(user.name)
+ end
end
describe 'filtering' do
@@ -115,11 +129,19 @@ describe 'Dropdown assignee', js: true, feature: true do
filtered_search.set('assignee:')
end
+ it 'filters by current user' do
+ page.within js_dropdown_assignee do
+ click_button 'Assigned to me'
+ end
+
+ expect(filtered_search.value).to eq("assignee:#{user.to_reference} ")
+ end
+
it 'fills in the assignee username when the assignee has not been filtered' do
click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}")
+ expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ")
end
it 'fills in the assignee username when the assignee has been filtered' do
@@ -127,14 +149,14 @@ describe 'Dropdown assignee', js: true, feature: true do
click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:@#{user.username}")
+ expect(filtered_search.value).to eq("assignee:@#{user.username} ")
end
it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:none")
+ expect(filtered_search.value).to eq("assignee:none ")
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 60a86cc93d4..a5d5d9d4c5e 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -66,6 +66,12 @@ describe 'Dropdown author', js: true, feature: true do
expect(dropdown_author_size).to eq(3)
end
+
+ it 'shows current user at top of dropdown' do
+ send_keys_to_filtered_search('author:')
+
+ expect(first('#js-dropdown-author li')).to have_content(user.name)
+ end
end
describe 'filtering' do
@@ -115,14 +121,14 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false)
- expect(filtered_search.value).to eq("author:@#{user_jacob.username}")
+ expect(filtered_search.value).to eq("author:@#{user_jacob.username} ")
end
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false)
- expect(filtered_search.value).to eq("author:@#{user.username}")
+ expect(filtered_search.value).to eq("author:@#{user.username} ")
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index 89c144141c9..71e0608a664 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -40,6 +40,16 @@ describe 'Dropdown label', js: true, feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
+ describe 'keyboard navigation' do
+ it 'selects label' do
+ send_keys_to_filtered_search('label:')
+
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq("label:~#{special_label.name}")
+ end
+ end
+
describe 'behavior' do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
@@ -159,7 +169,7 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{bug_label.title}")
+ expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name when the label is partially filled' do
@@ -167,49 +177,49 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{bug_label.title}")
+ expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"")
+ expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
end
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"")
+ expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
end
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'")
+ expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
end
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{uppercase_label.title}")
+ expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
end
it 'fills in the label name with special characters' do
click_label(special_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:~#{special_label.title}")
+ expect(filtered_search.value).to eq("label:~#{special_label.title} ")
end
it 'selects `no label`' do
find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click
expect(page).to have_css(js_dropdown_label, visible: false)
- expect(filtered_search.value).to eq("label:none")
+ expect(filtered_search.value).to eq("label:none ")
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index e5a271b663f..134e58ad586 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -127,7 +127,7 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{milestone.title}")
+ expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
end
it 'fills in the milestone name when the milestone is partially filled' do
@@ -135,56 +135,56 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{milestone.title}")
+ expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
end
it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"")
+ expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ")
end
it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"")
+ expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ")
end
it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'")
+ expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ")
end
it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}")
+ expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ")
end
it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}")
+ expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ")
end
it 'selects `no milestone`' do
click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:none")
+ expect(filtered_search.value).to eq("milestone:none ")
end
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:upcoming")
+ expect(filtered_search.value).to eq("milestone:upcoming ")
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 ead43d6784a..f48a0193545 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -19,9 +19,12 @@ describe 'Filter issues', js: true, feature: true do
let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
let(:filtered_search) { find('.filtered-search') }
- def input_filtered_search(search_term)
+ def input_filtered_search(search_term, submit: true)
filtered_search.set(search_term)
- filtered_search.send_keys(:enter)
+
+ if submit
+ filtered_search.send_keys(:enter)
+ end
end
def expect_filtered_search_input(input)
@@ -43,6 +46,10 @@ describe 'Filter issues', js: true, feature: true do
end
end
+ def select_search_at_index(pos)
+ evaluate_script("el = document.querySelector('.filtered-search'); el.focus(); el.setSelectionRange(#{pos}, #{pos});")
+ end
+
before do
project.team << [user, :master]
project.team << [user2, :master]
@@ -522,6 +529,44 @@ describe 'Filter issues', js: true, feature: true do
end
end
+ describe 'overwrites selected filter' do
+ it 'changes author' do
+ input_filtered_search("author:@#{user.username}", submit: false)
+
+ select_search_at_index(3)
+
+ page.within '#js-dropdown-author' do
+ click_button user2.username
+ end
+
+ expect(filtered_search.value).to eq("author:@#{user2.username} ")
+ end
+
+ it 'changes label' do
+ input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false)
+
+ select_search_at_index(27)
+
+ page.within '#js-dropdown-label' do
+ click_button label.name
+ end
+
+ expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
+ end
+
+ it 'changes label correctly space is in previous label' do
+ input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false)
+
+ select_search_at_index(0)
+
+ page.within '#js-dropdown-label' do
+ click_button label.name
+ end
+
+ expect(filtered_search.value).to eq("label:~#{label.name} ")
+ end
+ end
+
describe 'filter issues by text' do
context 'only text' do
it 'filters issues by searched text' do
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 56b1d354eb0..90eb60eb337 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -20,6 +20,22 @@ describe 'Search bar', js: true, feature: true do
left_style.to_s.gsub('left: ', '').to_f
end
+ describe 'keyboard navigation' do
+ it 'makes item active' do
+ filtered_search.native.send_keys(:down)
+
+ page.within '#js-dropdown-hint' do
+ expect(page).to have_selector('.dropdown-active')
+ end
+ end
+
+ it 'selects item' do
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq('author:')
+ end
+ end
+
describe 'clear search button' do
it 'clears text' do
search_text = 'search_text'
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 82c9bd0e6e6..31156fcf994 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -33,6 +33,45 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).not_to have_selector('.atwho-view')
end
+ it 'doesnt select the first item for non-assignee dropdowns' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys(':')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type')
+ end
+
+ it 'selects the first item for assignee dropdowns' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys('@')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-64')).to have_selector('.cur:first-of-type')
+ end
+
+ it 'selects the first item for non-assignee dropdowns if a query is entered' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys(':1')
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-58')).to have_selector('.cur:first-of-type')
+ end
+
context 'if a selected value has special characters' do
it 'wraps the result in double quotes' do
note = find('#note_note')
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 31f75512f4a..0a9cd11ad6e 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -100,6 +100,58 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
end
end
+ describe 'Issuable time tracking' do
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'Issue' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+
+ context 'Merge Request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+ end
+
+ describe 'Issuable time tracking' do
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'Issue' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+
+ context 'Merge Request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it_behaves_like 'issuable time tracker'
+ end
+ end
+
describe 'toggling the WIP prefix from the title from note' do
let(:issue) { create(:issue, project: project) }
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
index 82bc5226d07..dfe7c910a10 100644
--- a/spec/features/merge_requests/cherry_pick_spec.rb
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe 'Cherry-pick Merge Requests' do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) }
before do
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index c46bd8d449f..cb3bc392903 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -40,5 +40,32 @@ feature 'Edit Merge Request', feature: true do
expect(page).to have_content 'Remove source branch'
end
+
+ it 'should preserve description textarea height', js: true do
+ long_description = %q(
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
+
+ Cras congue nec ligula tristique viverra. Curabitur fringilla fringilla fringilla. Donec rhoncus dignissim orci ut accumsan. Ut rutrum urna a rhoncus varius. Maecenas blandit, mauris nec accumsan gravida, augue nibh finibus magna, sed maximus turpis libero nec neque. Suspendisse at semper est. Nunc imperdiet dapibus dui, varius sollicitudin erat luctus non. Sed pellentesque ligula eget posuere facilisis. Donec dictum commodo volutpat. Donec egestas dui ac magna sollicitudin bibendum. Vivamus purus neque, ullamcorper ac feugiat et, tempus sit amet metus. Praesent quis viverra neque. Sed bibendum viverra est, eu aliquam mi ornare vitae. Proin et dapibus ipsum. Nunc tortor diam, malesuada nec interdum vel, placerat quis justo. Ut viverra at erat eu laoreet.
+
+ Pellentesque commodo, diam sit amet dignissim condimentum, tortor justo pretium est, non venenatis metus eros ut nunc. Etiam ut neque eget sem dapibus aliquam. Curabitur vel elit lorem. Nulla nec enim elit. Sed ut ex id justo facilisis convallis at ac augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nullam cursus egestas turpis non tristique. Suspendisse in erat sem. Fusce libero elit, fermentum gravida mauris id, auctor iaculis felis. Nullam vulputate tempor laoreet.
+
+ Nam tempor et magna sed convallis. Fusce sit amet sollicitudin risus, a ullamcorper lacus. Morbi gravida quis sem eget porttitor. Donec eu egestas mauris, in elementum tortor. Sed eget ex mi. Mauris iaculis tortor ut est auctor, nec dignissim quam sagittis. Suspendisse vel metus non quam suscipit tincidunt. Cras molestie lacus non justo finibus sodales quis vitae erat. In a porttitor nisi, id sollicitudin urna. Ut at felis tellus. Suspendisse potenti.
+
+ Maecenas leo ligula, varius at neque vitae, ornare maximus justo. Nullam convallis luctus risus et vulputate. Duis suscipit faucibus iaculis. Etiam quis tortor faucibus, tristique tellus sit amet, sodales neque. Nulla dapibus nisi vel aliquet consequat. Etiam faucibus, metus eget condimentum iaculis, enim urna lobortis sem, id efficitur eros sapien nec nisi. Aenean ut finibus ex.
+ )
+
+ fill_in 'merge_request_description', with: long_description
+
+ height = get_textarea_height
+ find('.js-md-preview-button').click
+ find('.js-md-write-button').click
+ new_height = get_textarea_height
+
+ expect(height).to eq(new_height)
+ end
+
+ def get_textarea_height
+ page.evaluate_script('document.getElementById("merge_request_description").offsetHeight')
+ end
end
end
diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
new file mode 100644
index 00000000000..f2f8f11ab28
--- /dev/null
+++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Merge immediately', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project,
+ author: user,
+ title: 'Bug NS-04')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
+ end
+
+ before { project.team << [user, :master] }
+
+ context 'when there is active pipeline for merge request' do
+ background do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ before do
+ login_as user
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+
+ it 'enables merge immediately' do
+ page.within '.mr-widget-body' do
+ find('.dropdown-toggle').click
+
+ click_link 'Merge Immediately'
+
+ expect(find('.js-merge-button')).to have_content('Merge in progress')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index aa24a905001..2ea9c317bd1 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -32,19 +32,61 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
expect(page).to have_button "Merge When Pipeline Succeeds"
end
- context "Merge When Pipeline Succeeds enabled" do
- before do
- click_button "Merge When Pipeline Succeeds"
+ describe 'enabling Merge When Pipeline Succeeds' do
+ shared_examples 'Merge When Pipeline Succeeds activator' do
+ it 'activates the Merge When Pipeline Succeeds feature' do
+ click_button "Merge When Pipeline Succeeds"
+
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
+ expect(page).to have_content "The source branch will not be removed."
+ expect(page).to have_link "Cancel Automatic Merge"
+ visit_merge_request(merge_request) # Needed to refresh the page
+ expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
+ end
end
- it 'activates Merge When Pipeline Succeeds feature' do
- expect(page).to have_link "Cancel Automatic Merge"
+ context "when enabled immediately" do
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
+ end
+
+ context 'when enabled after pipeline status changed' do
+ before do
+ pipeline.run!
- expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will not be removed."
+ # We depend on merge request widget being reloaded
+ # so we have to wait for asynchronous call to reload it
+ # and have_content expectation handles that.
+ #
+ expect(page).to have_content "Pipeline ##{pipeline.id} running"
+ end
+
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
+ end
+
+ context 'when enabled after it was previously canceled' do
+ before do
+ click_button "Merge When Pipeline Succeeds"
+ click_link "Cancel Automatic Merge"
+ end
+
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
+ end
- visit_merge_request(merge_request) # Needed to refresh the page
- expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
+ context 'when it was enabled and then canceled' do
+ let(:merge_request) do
+ create(:merge_request_with_diffs,
+ :merge_when_build_succeeds,
+ source_project: project,
+ title: 'Bug NS-04',
+ author: user,
+ merge_user: user)
+ end
+
+ before do
+ click_link "Cancel Automatic Merge"
+ end
+
+ it_behaves_like 'Merge When Pipeline Succeeds activator'
end
end
end
diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_requests/wip_message_spec.rb
new file mode 100644
index 00000000000..3311731b33b
--- /dev/null
+++ b/spec/features/merge_requests/wip_message_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+feature 'Work In Progress help message', feature: true do
+ let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'with WIP commits' do
+ it 'shows a specific WIP hint' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'wip',
+ target_branch: 'master'
+ }
+ )
+
+ within_wip_explanation do
+ expect(page).to have_text(
+ 'It looks like you have some WIP commits in this branch'
+ )
+ end
+ end
+ end
+
+ context 'without WIP commits' do
+ it 'shows the regular WIP message' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ }
+ )
+
+ within_wip_explanation do
+ expect(page).not_to have_text(
+ 'It looks like you have some WIP commits in this branch'
+ )
+ expect(page).to have_text(
+ "Start the title with WIP: to prevent a Work In Progress merge \
+request from being merged before it's ready"
+ )
+ end
+ end
+ end
+
+ def within_wip_explanation(&block)
+ page.within '.js-no-wip-explanation' do
+ yield
+ end
+ end
+end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index d46d9e9399e..7baf7913424 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
include WaitForAjax
describe 'Cherry-pick Commits' do
- let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
new file mode 100644
index 00000000000..d0bafc6168c
--- /dev/null
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do
+ let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+
+ let(:project) { create(:empty_project) }
+
+ background do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ context 'admin user' do
+ before do
+ login_as(:admin)
+ end
+
+ context 'moving the namespace' do
+ scenario 'removes the export file' do
+ setup_export_project
+
+ old_export_path = project.export_path.dup
+
+ expect(File).to exist(old_export_path)
+
+ project.namespace.update(path: 'new_path')
+
+ expect(File).not_to exist(old_export_path)
+ end
+ end
+
+ context 'deleting the namespace' do
+ scenario 'removes the export file' do
+ setup_export_project
+
+ old_export_path = project.export_path.dup
+
+ expect(File).to exist(old_export_path)
+
+ project.namespace.destroy
+
+ expect(File).not_to exist(old_export_path)
+ end
+ end
+
+ def setup_export_project
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Export project')
+
+ click_link 'Export project'
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Download export')
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 14e009daba8..e673ece37c3 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -11,18 +11,42 @@ describe 'Pipeline', :feature, :js do
project.team << [user, :developer]
end
+ shared_context 'pipeline builds' do
+ let!(:build_passed) do
+ create(:ci_build, :success,
+ pipeline: pipeline, stage: 'build', name: 'build')
+ end
+
+ let!(:build_failed) do
+ create(:ci_build, :failed,
+ pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ end
+
+ let!(:build_running) do
+ create(:ci_build, :running,
+ pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ end
+
+ let!(:build_manual) do
+ create(:ci_build, :manual,
+ pipeline: pipeline, stage: 'deploy', name: 'manual-build')
+ end
+
+ let!(:build_external) do
+ create(:generic_commit_status, status: 'success',
+ pipeline: pipeline,
+ name: 'jenkins',
+ stage: 'external',
+ target_url: 'http://gitlab.com/status')
+ end
+ end
+
describe 'GET /:project/pipelines/:id' do
+ include_context 'pipeline builds'
+
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
- before do
- @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
- @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
- @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build')
- @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
- end
-
before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
it 'shows the pipeline graph' do
@@ -116,6 +140,7 @@ describe 'Pipeline', :feature, :js do
it 'shows the success icon and the generic comit status build' do
expect(page).to have_selector('.ci-status-icon-success')
expect(page).to have_content('jenkins')
+ expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
end
end
end
@@ -157,26 +182,22 @@ describe 'Pipeline', :feature, :js do
end
describe 'GET /:project/pipelines/:id/builds' do
+ include_context 'pipeline builds'
+
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
- @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
- @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
- @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
- @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build')
- @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)
end
- before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)}
-
it 'shows a list of builds' do
expect(page).to have_content('Test')
- expect(page).to have_content(@success.id)
+ expect(page).to have_content(build_passed.id)
expect(page).to have_content('Deploy')
- expect(page).to have_content(@failed.id)
- expect(page).to have_content(@running.id)
- expect(page).to have_content(@external.id)
+ expect(page).to have_content(build_failed.id)
+ expect(page).to have_content(build_running.id)
+ expect(page).to have_content(build_external.id)
expect(page).to have_content('Retry failed')
expect(page).to have_content('Cancel running')
expect(page).to have_link('Play')
@@ -230,7 +251,7 @@ describe 'Pipeline', :feature, :js do
end
end
- it { expect(@manual.reload).to be_pending }
+ it { expect(build_manual.reload).to be_pending }
end
end
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
new file mode 100644
index 00000000000..cef315ac9cd
--- /dev/null
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Visibility settings', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) }
+
+ context 'as owner' do
+ before do
+ login_as(user)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'project visibility select is available' do
+ visibility_select_container = find('.js-visibility-select')
+
+ expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
+ expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
+ end
+
+ scenario 'project visibility description updates on change' do
+ visibility_select_container = find('.js-visibility-select')
+ visibility_select = visibility_select_container.find('.visibility-select')
+ visibility_select.select('Private')
+
+ expect(visibility_select.value).to eq '0'
+ expect(visibility_select_container).to have_content 'Project access must be granted explicitly to each user.'
+ end
+ end
+
+ context 'as master' do
+ let(:master_user) { create(:user) }
+
+ before do
+ project.team << [master_user, :master]
+ login_as(master_user)
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'project visibility is locked' do
+ visibility_select_container = find('.js-visibility-select')
+
+ expect(visibility_select_container).not_to have_select '.visibility-select'
+ expect(visibility_select_container).to have_content 'Public'
+ expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
+ end
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index a05b83959fb..0fe5a897565 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -211,4 +211,44 @@ describe "Search", feature: true do
end
end
end
+
+ describe 'search for commits' do
+ before do
+ visit search_path(project_id: project.id)
+ end
+
+ it 'redirects to commit page when search by sha and only commit found' do
+ fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f'
+
+ click_button 'Search'
+
+ expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
+ end
+
+ it 'redirects to single commit regardless of query case' do
+ fill_in 'search', with: '6D394385cf'
+
+ click_button 'Search'
+
+ expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
+ end
+
+ it 'holds on /search page when the only commit is found by message' do
+ create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master')
+
+ fill_in 'search', with: 'deadbeef'
+ click_button 'Search'
+
+ expect(page).to have_current_path('/search', only_path: true)
+ end
+
+ it 'shows multiple matching commits' do
+ fill_in 'search', with: 'See merge request'
+
+ click_button 'Search'
+ click_link 'Commits'
+
+ expect(page).to have_selector('.commit-row-description', count: 9)
+ end
+ end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index ecebabefff8..92d5a2fbc48 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -262,8 +262,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/hooks" do
- subject { namespace_project_hooks_path(project.namespace, project) }
+ describe "GET /:project_path/settings/integrations" do
+ subject { namespace_project_settings_integrations_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 9bc59a7c4f9..b616e488487 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -234,8 +234,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
- describe "GET /:project_path/hooks" do
- subject { namespace_project_hooks_path(project.namespace, project) }
+ describe "GET /:project_path/namespace/hooks" do
+ subject { namespace_project_settings_integrations_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index a8d43b3d581..ded85e837f4 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -400,8 +400,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:visitor) }
end
- describe "GET /:project_path/hooks" do
- subject { namespace_project_hooks_path(project.namespace, project) }
+ describe "GET /:project_path/settings/integrations" do
+ subject { namespace_project_settings_integrations_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index abb27c90e0a..a5d14aa19f1 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -36,6 +36,19 @@ feature 'Task Lists', feature: true do
MARKDOWN
end
+ let(:nested_tasks_markdown) do
+ <<-EOT.strip_heredoc
+ - [ ] Task a
+ - [x] Task a.1
+ - [ ] Task a.2
+ - [ ] Task b
+
+ 1. [ ] Task 1
+ 1. [ ] Task 1.1
+ 1. [x] Task 1.2
+ EOT
+ end
+
before do
Warden.test_mode!
@@ -123,6 +136,35 @@ feature 'Task Lists', feature: true do
expect(page).to have_content("1 of 1 task completed")
end
end
+
+ describe 'nested tasks', js: true do
+ let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
+
+ before { visit_issue(project, issue) }
+
+ it 'renders' do
+ expect(page).to have_selector('ul.task-list', count: 2)
+ expect(page).to have_selector('li.task-list-item', count: 7)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ expect(page).to have_selector('ol input[checked]', count: 1)
+ end
+
+ it 'solves tasks' do
+ expect(page).to have_content("2 of 7 tasks completed")
+
+ page.find('li.task-list-item', text: 'Task b').find('input').click
+ page.find('li.task-list-item ul li.task-list-item', text: 'Task a.2').find('input').click
+ page.find('li.task-list-item ol li.task-list-item', text: 'Task 1.1').find('input').click
+
+ expect(page).to have_content("5 of 7 tasks completed")
+
+ visit_issue(project, issue) # reload to see new system notes
+
+ expect(page).to have_content('marked the task Task b as complete')
+ expect(page).to have_content('marked the task Task a.2 as complete')
+ expect(page).to have_content('marked the task Task 1.1 as complete')
+ end
+ end
end
describe 'for Notes' do
@@ -236,7 +278,7 @@ feature 'Task Lists', feature: true do
expect(page).to have_content("2 of 6 tasks completed")
end
end
-
+
describe 'single incomplete task' do
let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 4bda0927692..3850e930b6d 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -165,7 +165,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows the todo' do
- expect(page).to have_content 'The build failed for your merge request'
+ expect(page).to have_content 'The build failed for merge request'
end
it 'links to the pipelines for the merge request' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 88361e27102..e4ba1d2f1c2 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequestsFinder do
let(:project1) { create(:project) }
let(:project2) { create(:project, forked_from_project: project1) }
- let(:project3) { create(:project, forked_from_project: project1, archived: true) }
+ let(:project3) { create(:project, :archived, forked_from_project: project1) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index fdce4e714ff..dea87980e25 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -2,13 +2,13 @@ require 'spec_helper'
describe MoveToProjectFinder do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
- let(:no_access_project) { create(:project) }
- let(:guest_project) { create(:project) }
- let(:reporter_project) { create(:project) }
- let(:developer_project) { create(:project) }
- let(:master_project) { create(:project) }
+ let(:no_access_project) { create(:empty_project) }
+ let(:guest_project) { create(:empty_project) }
+ let(:reporter_project) { create(:empty_project) }
+ let(:developer_project) { create(:empty_project) }
+ let(:master_project) { create(:empty_project) }
subject { described_class.new(user) }
@@ -36,8 +36,8 @@ describe MoveToProjectFinder do
it 'does not return archived projects' do
reporter_project.team << [user, :reporter]
- reporter_project.update_attributes(archived: true)
- other_reporter_project = create(:project)
+ reporter_project.archive!
+ other_reporter_project = create(:empty_project)
other_reporter_project.team << [user, :reporter]
expect(subject.execute(project).to_a).to eq([other_reporter_project])
@@ -46,7 +46,7 @@ describe MoveToProjectFinder do
it 'does not return projects for which issues are disabled' do
reporter_project.team << [user, :reporter]
reporter_project.update_attributes(issues_enabled: false)
- other_reporter_project = create(:project)
+ other_reporter_project = create(:empty_project)
other_reporter_project.team << [user, :reporter]
expect(subject.execute(project).to_a).to eq([other_reporter_project])
@@ -83,10 +83,10 @@ describe MoveToProjectFinder do
end
it 'returns projects matching a search query' do
- foo_project = create(:project)
+ foo_project = create(:empty_project)
foo_project.team << [user, :master]
- wadus_project = create(:project, name: 'wadus')
+ wadus_project = create(:empty_project, name: 'wadus')
wadus_project.team << [user, :master]
expect(subject.execute(project).to_a).to eq([wadus_project, foo_project])
diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/metrics_spec.rb
new file mode 100644
index 00000000000..bb595162370
--- /dev/null
+++ b/spec/initializers/metrics_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require_relative '../../config/initializers/metrics'
+
+describe 'instrument_classes', lib: true do
+ let(:config) { double(:config) }
+
+ before do
+ allow(config).to receive(:instrument_method)
+ allow(config).to receive(:instrument_methods)
+ allow(config).to receive(:instrument_instance_methods)
+ end
+
+ it 'can autoload and instrument all files' do
+ expect { instrument_classes(config) }.not_to raise_error
+ end
+end
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index dcbcd014dc3..3cd419b37c9 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -23,6 +23,8 @@
"plugins": ["jasmine"],
"rules": {
"prefer-arrow-callback": 0,
- "func-names": 0
+ "func-names": 0,
+ "jasmine/no-suite-dupes": [1, "branch"],
+ "jasmine/no-spec-dupes": [1, "branch"]
}
}
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index cf19aa05031..a2d57824585 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -21,7 +21,6 @@
messages = $('.abuse-reports .message');
});
-
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
index b3617a45bd4..7bc5b3268a0 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */
+/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */
/*= require js.cookie.js */
/*= require jquery.endless-scroll.js */
@@ -19,18 +19,18 @@
name: 'merge events',
}, {
id: 'comments',
- },{
+ }, {
id: 'team',
}];
function getEventName(index) {
- let filter = filters[index];
+ const filter = filters[index];
return filter.hasOwnProperty('name') ? filter.name : filter.id;
}
function getSelector(index) {
- let filter = filters[index];
- return `#${filter.id}_event_filter`
+ const filter = filters[index];
+ return `#${filter.id}_event_filter`;
}
describe('Activities', () => {
@@ -39,17 +39,17 @@
new gl.Activities();
});
- for(let i = 0; i < filters.length; i++) {
+ 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++) {
+ for (let x = 0; x < filters.length; x += 1) {
((x) => {
- let shouldHighlight = i === x;
- let testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+ const shouldHighlight = i === x;
+ const testName = shouldHighlight ? 'should highlight' : 'should not highlight';
it(`${testName} ${getEventName(x)}`, () => {
expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index faba2837d41..71446b9df61 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
/* global AwardsHandler */
/*= require awards_handler */
@@ -231,5 +231,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index e77d732a32a..51d911792ba 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */
/*= require behaviors/autosize */
@@ -18,5 +18,4 @@
return $(document).trigger('page:load');
};
});
-
}).call(this);
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 1a1f34cfdc0..0f046c2d83a 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */
/*= require behaviors/quick_submit */
@@ -93,5 +93,4 @@
return $.Event('keydown', $.extend({}, defaults, options));
};
});
-
}).call(this);
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 1f62591c06d..9467056f04c 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var */
/*= require behaviors/requires_input */
@@ -41,5 +41,4 @@
return expect(spy).toHaveBeenCalled();
});
});
-
}).call(this);
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index b3a1afa28a5..7c5850111cb 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */
+/* eslint-disable comma-dangle, one-var, no-unused-vars */
/* global Vue */
/* global BoardService */
/* global boardsMockInterceptor */
@@ -146,8 +146,8 @@ describe('Store', () => {
});
it('moves the position of lists', () => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
@@ -157,8 +157,8 @@ describe('Store', () => {
});
it('moves an issue from one list to another', (done) => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
index 3f6b328348d..4d851b2d320 100644
--- a/spec/javascripts/dashboard_spec.js.es6
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-new, padded-blocks */
+/* eslint-disable no-new */
/*= require sidebar */
/*= require jquery */
@@ -36,5 +36,4 @@
expect(todosCountText()).toEqual('1,000,000');
});
});
-
})(window.gl);
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
index 18805d26ac0..fbfa34a5da7 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
+/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
/* global CommentsStore */
//= require vue
@@ -9,7 +9,7 @@
(() => {
function createDiscussion(noteId = 1, resolved = true) {
CommentsStore.create('a', noteId, true, resolved, 'test');
- };
+ }
beforeEach(() => {
CommentsStore.state = {};
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
index 21241116e29..95796f23894 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -33,7 +33,6 @@ describe('Rollback Component', () => {
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
});
-
it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6
new file mode 100644
index 00000000000..20e11ca3738
--- /dev/null
+++ b/spec/javascripts/environments/environment_spec.js.es6
@@ -0,0 +1,127 @@
+/* global Vue, environment */
+
+//= require vue
+//= require vue-resource
+//= require flash
+//= require environments/stores/environments_store
+//= require environments/components/environment
+//= require ./mock_data
+
+describe('Environment', () => {
+ preloadFixtures('environments/environments');
+
+ let component;
+
+ beforeEach(() => {
+ loadFixtures('environments/environments');
+ });
+
+ describe('successfull request', () => {
+ describe('without environments', () => {
+ const environmentsEmptyResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsEmptyResponseInterceptor,
+ );
+ });
+
+ it('should render the empty state', (done) => {
+ component = new gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: gl.environmentsList.EnvironmentsStore.create(),
+ },
+ });
+
+ 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);
+ });
+ });
+
+ describe('with environments', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([environment]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ });
+
+ it('should render a table with environments', (done) => {
+ component = new gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: gl.environmentsList.EnvironmentsStore.create(),
+ },
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('table tbody tr').length,
+ ).toEqual(1);
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const environmentsErrorResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsErrorResponseInterceptor,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ component = new gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: gl.environmentsList.EnvironmentsStore.create(),
+ },
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-blank-state-title').textContent,
+ ).toContain('You don\'t have any environments right now.');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
index 9e16bc3e6a5..8ecd01f9a83 100644
--- a/spec/javascripts/environments/mock_data.js.es6
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -133,3 +133,17 @@ const environmentsList = [
updated_at: '2016-11-07T11:11:16.525Z',
},
];
+
+const environment = {
+ id: 4,
+ name: 'production',
+ state: 'available',
+ external_url: 'http://production.',
+ environment_type: null,
+ last_deployment: {},
+ 'stoppable?': false,
+ environment_path: '/root/review-app/environments/4',
+ stop_path: '/root/review-app/environments/4/stop',
+ created_at: '2016-12-16T11:51:04.690Z',
+ updated_at: '2016-12-16T12:04:51.133Z',
+};
diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6
index 2ec759c8e80..3949c5615d5 100644
--- a/spec/javascripts/extensions/array_spec.js.es6
+++ b/spec/javascripts/extensions/array_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var */
/*= require extensions/array */
@@ -42,5 +42,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index 91846bb9143..5cd0e5ab0f0 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var */
/*= require extensions/jquery */
@@ -39,5 +39,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
index ce61b73aa8a..19bd8d53219 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
@@ -31,41 +31,68 @@
});
describe('filterWithSymbol', () => {
+ let input;
const item = {
title: '@root',
};
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
+
+ input = document.getElementById('test');
+ });
+
it('should filter without symbol', () => {
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo');
+ input.value = ':roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with symbol', () => {
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo');
+ input.value = '@roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with colon', () => {
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':');
+ input.value = 'roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
});
});
describe('filterHint', () => {
+ let input;
+
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
+
+ input = document.getElementById('test');
+ });
+
it('should filter', () => {
- let updatedItem = gl.DropdownUtils.filterHint({
+ input.value = 'l';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
hint: 'label',
- }, 'l');
+ });
expect(updatedItem.droplab_hidden).toBe(false);
- updatedItem = gl.DropdownUtils.filterHint({
+ input.value = 'o';
+ updatedItem = gl.DropdownUtils.filterHint(input, {
hint: 'label',
}, 'o');
expect(updatedItem.droplab_hidden).toBe(true);
});
it('should return droplab_hidden false when item has no hint', () => {
- const updatedItem = gl.DropdownUtils.filterHint({}, '');
+ const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
expect(updatedItem.droplab_hidden).toBe(false);
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
index d0d27ceb4a6..4bd45eb457d 100644
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
@@ -31,7 +31,7 @@
it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
- expect(getInputValue()).toBe('label:none');
+ expect(getInputValue()).toBe('label:none ');
});
});
@@ -45,13 +45,13 @@
it('should replace tokenValue', () => {
setInputValue('author:roo');
gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
- expect(getInputValue()).toBe('author:@root');
+ expect(getInputValue()).toBe('author:@root ');
});
it('should add tokenValues containing spaces', () => {
setInputValue('label:~"test');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
- expect(getInputValue()).toBe('label:~\'"test me"\'');
+ expect(getInputValue()).toBe('label:~\'"test me"\' ');
});
});
});
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
index 3d776bb9277..2ef242901e8 100644
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -1,5 +1,4 @@
-/* eslint-disable space-before-function-paren, padded-blocks */
+/* eslint-disable space-before-function-paren */
(function() {
window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
-
}).call(this);
diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml
index d89bc50c1f0..e6000fbb553 100644
--- a/spec/javascripts/fixtures/environments/environments.html.haml
+++ b/spec/javascripts/fixtures/environments/environments.html.haml
@@ -1,5 +1,5 @@
%div
- #environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments",
+ #environments-list-view{ data: { environments_data: "foo/environments",
"can-create-deployment" => "true",
"can-read-environment" => "true",
"can-create-environment" => "true",
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
index e9bf7568e95..29370b974af 100644
--- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
+++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
@@ -1,8 +1,9 @@
-%div.js-builds-dropdown-tests
- %button.dropdown.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar'}
+%div.js-builds-dropdown-tests.dropdown.dropdown.js-mini-pipeline-graph
+ %button.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar', data: { toggle: 'dropdown'} }
Dropdown
- %div.js-builds-dropdown-container
- %div.js-builds-dropdown-list
- %div.js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6
new file mode 100644
index 00000000000..6b48d82cb23
--- /dev/null
+++ b/spec/javascripts/gfm_auto_complete_spec.js.es6
@@ -0,0 +1,65 @@
+//= require gfm_auto_complete
+//= require jquery
+//= require jquery.atwho
+
+const global = window.gl || (window.gl = {});
+const GfmAutoComplete = global.GfmAutoComplete;
+
+describe('GfmAutoComplete', function () {
+ describe('DefaultOptions.sorter', function () {
+ describe('assets loading', function () {
+ beforeEach(function () {
+ spyOn(GfmAutoComplete, 'isLoading').and.returnValue(true);
+
+ this.atwhoInstance = { setting: {} };
+ this.items = [];
+
+ this.sorterValue = GfmAutoComplete.DefaultOptions.sorter
+ .call(this.atwhoInstance, '', this.items);
+ });
+
+ it('should disable highlightFirst', function () {
+ expect(this.atwhoInstance.setting.highlightFirst).toBe(false);
+ });
+
+ it('should return the passed unfiltered items', function () {
+ expect(this.sorterValue).toEqual(this.items);
+ });
+ });
+
+ describe('assets finished loading', function () {
+ beforeEach(function () {
+ spyOn(GfmAutoComplete, 'isLoading').and.returnValue(false);
+ spyOn($.fn.atwho.default.callbacks, 'sorter');
+ });
+
+ it('should enable highlightFirst if alwaysHighlightFirst is set', function () {
+ const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance);
+
+ expect(atwhoInstance.setting.highlightFirst).toBe(true);
+ });
+
+ it('should enable highlightFirst if a query is present', function () {
+ const atwhoInstance = { setting: {} };
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query');
+
+ expect(atwhoInstance.setting.highlightFirst).toBe(true);
+ });
+
+ it('should call the default atwho sorter', function () {
+ const atwhoInstance = { setting: {} };
+
+ const query = 'query';
+ const items = [];
+ const searchKey = 'searchKey';
+
+ GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey);
+
+ expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index d11b1182d9a..06fa64b1b4e 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */
+/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
/* global Turbolinks */
/*= require jquery */
@@ -22,7 +22,7 @@
let remoteCallback;
- let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ const navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
i = i || 0;
if (!i) direction = direction.toUpperCase();
$('body').trigger({
@@ -30,7 +30,7 @@
which: ARROW_KEYS[direction],
keyCode: ARROW_KEYS[direction]
});
- i++;
+ i += 1;
if (i <= steps) {
navigateWithKeys(direction, steps, cb, i);
} else {
@@ -38,9 +38,9 @@
}
};
- let remoteMock = function remoteMock(data, term, callback) {
+ const remoteMock = function remoteMock(data, term, callback) {
remoteCallback = callback.bind({}, data);
- }
+ };
describe('Dropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
@@ -89,7 +89,7 @@
it('should select a following item on DOWN keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
navigateWithKeys('down', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
@@ -100,7 +100,7 @@
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
navigateWithKeys('down', (this.projectsData.length - 1), () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
@@ -109,15 +109,15 @@
});
it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open')
- let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
spyOn(Turbolinks, 'visit').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
- let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
+ const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
- let linkedLocation = link.attr('href');
+ const linkedLocation = link.attr('href');
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
});
});
@@ -140,18 +140,18 @@
this.dropdownButtonElement.click();
});
- it('should not focus search input while remote task is not complete', ()=> {
+ it('should not focus search input while remote task is not complete', () => {
expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
remoteCallback();
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
- it('should focus search input after remote task is complete', ()=> {
+ it('should focus search input after remote task is complete', () => {
remoteCallback();
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
- it('should focus on input when opening for the second time', ()=> {
+ it('should focus on input when opening for the second time', () => {
remoteCallback();
this.dropdownContainerElement.trigger({
type: 'keyup',
@@ -164,16 +164,15 @@
});
describe('input focus with array data', () => {
- it('should focus input when passing array data to drop down', ()=> {
+ it('should focus input when passing array data to drop down', () => {
initDropDown.call(this, false, true);
this.dropdownButtonElement.click();
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
});
-
it('should still have input value on close and restore', () => {
- let $searchInput = $(SEARCH_INPUT_SELECTOR);
+ const $searchInput = $(SEARCH_INPUT_SELECTOR);
initDropDown.call(this, false, true);
$searchInput
.trigger('focus')
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
index e5d934540af..f68fd9e00d7 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */
+/* eslint-disable space-before-function-paren, arrow-body-style */
//= require jquery
//= require gl_field_errors
@@ -28,7 +28,7 @@
expect(customErrorElem.length).toBe(1);
const customErrors = this.fieldErrors.state.inputs.filter((input) => {
- return input.inputElement.hasClass(customErrorFlag);
+ return input.inputElement.hasClass(customErrorFlag);
});
expect(customErrors.length).toBe(0);
});
@@ -107,7 +107,5 @@
expect(noTitleErrorElem.text()).toBe('This field is required.');
expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
});
-
});
-
})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index bc5cbeb6a40..d76fcc5206a 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
+/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */
/* global d3 */
/* global ContributorsGraph */
/* global ContributorsMasterGraph */
@@ -8,126 +8,123 @@
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
it("set the x_domain", function () {
- ContributorsGraph.set_x_domain(20)
- expect(ContributorsGraph.prototype.x_domain).toEqual(20)
- })
- })
+ ContributorsGraph.set_x_domain(20);
+ expect(ContributorsGraph.prototype.x_domain).toEqual(20);
+ });
+ });
describe("#set_y_domain", function () {
it("sets the y_domain", function () {
- ContributorsGraph.set_y_domain([{commits: 30}])
- expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30])
- })
- })
+ ContributorsGraph.set_y_domain([{ commits: 30 }]);
+ expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
+ });
+ });
describe("#init_x_domain", function () {
it("sets the initial x_domain", function () {
- ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}])
- expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"])
- })
- })
+ ContributorsGraph.init_x_domain([{ date: "2013-01-31" }, { date: "2012-01-31" }]);
+ expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]);
+ });
+ });
describe("#init_y_domain", function () {
it("sets the initial y_domain", function () {
- ContributorsGraph.init_y_domain([{commits: 30}])
- expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30])
- })
- })
+ ContributorsGraph.init_y_domain([{ commits: 30 }]);
+ expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
+ });
+ });
describe("#init_domain", function () {
it("calls init_x_domain and init_y_domain", function () {
- spyOn(ContributorsGraph, "init_x_domain")
- spyOn(ContributorsGraph, "init_y_domain")
- ContributorsGraph.init_domain()
- expect(ContributorsGraph.init_x_domain).toHaveBeenCalled()
- expect(ContributorsGraph.init_y_domain).toHaveBeenCalled()
- })
- })
+ spyOn(ContributorsGraph, "init_x_domain");
+ spyOn(ContributorsGraph, "init_y_domain");
+ ContributorsGraph.init_domain();
+ expect(ContributorsGraph.init_x_domain).toHaveBeenCalled();
+ expect(ContributorsGraph.init_y_domain).toHaveBeenCalled();
+ });
+ });
describe("#set_dates", function () {
it("sets the dates", function () {
- ContributorsGraph.set_dates("2013-12-01")
- expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01")
- })
- })
+ ContributorsGraph.set_dates("2013-12-01");
+ expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01");
+ });
+ });
describe("#set_x_domain", function () {
it("sets the instance's x domain using the prototype's x_domain", function () {
- ContributorsGraph.prototype.x_domain = 20
- var instance = new ContributorsGraph()
- instance.x = d3.time.scale().range([0, 100]).clamp(true)
- spyOn(instance.x, 'domain')
- instance.set_x_domain()
- expect(instance.x.domain).toHaveBeenCalledWith(20)
- })
- })
+ ContributorsGraph.prototype.x_domain = 20;
+ var instance = new ContributorsGraph();
+ instance.x = d3.time.scale().range([0, 100]).clamp(true);
+ spyOn(instance.x, 'domain');
+ instance.set_x_domain();
+ expect(instance.x.domain).toHaveBeenCalledWith(20);
+ });
+ });
describe("#set_y_domain", function () {
it("sets the instance's y domain using the prototype's y_domain", function () {
- ContributorsGraph.prototype.y_domain = 30
- var instance = new ContributorsGraph()
- instance.y = d3.scale.linear().range([100, 0]).nice()
- spyOn(instance.y, 'domain')
- instance.set_y_domain()
- expect(instance.y.domain).toHaveBeenCalledWith(30)
- })
- })
+ ContributorsGraph.prototype.y_domain = 30;
+ var instance = new ContributorsGraph();
+ instance.y = d3.scale.linear().range([100, 0]).nice();
+ spyOn(instance.y, 'domain');
+ instance.set_y_domain();
+ expect(instance.y.domain).toHaveBeenCalledWith(30);
+ });
+ });
describe("#set_domain", function () {
it("calls set_x_domain and set_y_domain", function () {
- var instance = new ContributorsGraph()
- spyOn(instance, 'set_x_domain')
- spyOn(instance, 'set_y_domain')
- instance.set_domain()
- expect(instance.set_x_domain).toHaveBeenCalled()
- expect(instance.set_y_domain).toHaveBeenCalled()
- })
- })
+ var instance = new ContributorsGraph();
+ spyOn(instance, 'set_x_domain');
+ spyOn(instance, 'set_y_domain');
+ instance.set_domain();
+ expect(instance.set_x_domain).toHaveBeenCalled();
+ expect(instance.set_y_domain).toHaveBeenCalled();
+ });
+ });
describe("#set_data", function () {
it("sets the data", function () {
- var instance = new ContributorsGraph()
- instance.set_data("20")
- expect(instance.data).toEqual("20")
- })
- })
-})
+ var instance = new ContributorsGraph();
+ instance.set_data("20");
+ expect(instance.data).toEqual("20");
+ });
+ });
+});
describe("ContributorsMasterGraph", function () {
-
// TODO: fix or remove
- //describe("#process_dates", function () {
- //it("gets and parses dates", function () {
- //var graph = new ContributorsMasterGraph()
- //var data = 'random data here'
- //spyOn(graph, 'parse_dates')
- //spyOn(graph, 'get_dates').andReturn("get")
- //spyOn(ContributorsGraph,'set_dates').andCallThrough()
- //graph.process_dates(data)
- //expect(graph.parse_dates).toHaveBeenCalledWith(data)
- //expect(graph.get_dates).toHaveBeenCalledWith(data)
- //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get")
- //})
- //})
+ // describe("#process_dates", function () {
+ // it("gets and parses dates", function () {
+ // var graph = new ContributorsMasterGraph();
+ // var data = 'random data here';
+ // spyOn(graph, 'parse_dates');
+ // spyOn(graph, 'get_dates').andReturn("get");
+ // spyOn(ContributorsGraph,'set_dates').andCallThrough();
+ // graph.process_dates(data);
+ // expect(graph.parse_dates).toHaveBeenCalledWith(data);
+ // expect(graph.get_dates).toHaveBeenCalledWith(data);
+ // expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get");
+ // });
+ // });
describe("#get_dates", function () {
it("plucks the date field from data collection", function () {
- var graph = new ContributorsMasterGraph()
- var data = [{date: "2013-01-01"}, {date: "2012-12-15"}]
- expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"])
- })
- })
+ var graph = new ContributorsMasterGraph();
+ var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }];
+ expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]);
+ });
+ });
describe("#parse_dates", function () {
it("parses the dates", function () {
- var graph = new ContributorsMasterGraph()
- var parseDate = d3.time.format("%Y-%m-%d").parse
- var data = [{date: "2013-01-01"}, {date: "2012-12-15"}]
- var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}]
- graph.parse_dates(data)
- expect(data).toEqual(correct)
- })
- })
-
-
-})
+ var graph = new ContributorsMasterGraph();
+ var parseDate = d3.time.format("%Y-%m-%d").parse;
+ var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }];
+ var correct = [{ date: parseDate(data[0].date) }, { date: parseDate(data[1].date) }];
+ graph.parse_dates(data);
+ expect(data).toEqual(correct);
+ });
+ });
+});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 751f3d175e2..63f28dfb8ad 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,215 +1,218 @@
-/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */
+/* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */
/* global ContributorsStatGraphUtil */
//= require graphs/stat_graph_contributors_util
describe("ContributorsStatGraphUtil", function () {
-
describe("#parse_log", function () {
it("returns a correctly parsed log", function () {
var fake_log = [
- {author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471},
- {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1},
- {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3},
- {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}]
+ { author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471 },
+ { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1 },
+ { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3 },
+ { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3 }
+ ];
var correct_parsed_log = {
total: [
- {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
- by_author:
- [
- {
- author_name: "Karlo Soriano", author_email: "karlo@email.com",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
- },
- {
- author_name: "Dmitriy Zaporozhets",author_email: "dzaporozhets@email.com",
- "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
- }
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ ],
+ by_author: [
+ {
+ author_name: "Karlo Soriano", author_email: "karlo@email.com",
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
+ },
+ {
+ author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com",
+ "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ }
]
- }
- expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log)
- })
- })
+ };
+ expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log);
+ });
+ });
describe("#store_data", function () {
-
- var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471}
- var fake_total = {}
- var fake_by_author = {}
+ var fake_entry = { author: "Karlo Soriano", date: "2013-05-09", additions: 471 };
+ var fake_total = {};
+ var fake_by_author = {};
it("calls #store_commits", function () {
- spyOn(ContributorsStatGraphUtil, 'store_commits')
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
- expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled()
- })
+ spyOn(ContributorsStatGraphUtil, 'store_commits');
+ ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
+ expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled();
+ });
it("calls #store_additions", function () {
- spyOn(ContributorsStatGraphUtil, 'store_additions')
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
- expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled()
- })
+ spyOn(ContributorsStatGraphUtil, 'store_additions');
+ ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
+ expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled();
+ });
it("calls #store_deletions", function () {
- spyOn(ContributorsStatGraphUtil, 'store_deletions')
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author)
- expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled()
- })
-
- })
+ spyOn(ContributorsStatGraphUtil, 'store_deletions');
+ ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
+ expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled();
+ });
+ });
// TODO: fix or remove
- //describe("#store_commits", function () {
- //var fake_total = "fake_total"
- //var fake_by_author = "fake_by_author"
-
- //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- //spyOn(ContributorsStatGraphUtil, 'add')
- //ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author)
- //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]])
- //})
- //})
+ // describe("#store_commits", function () {
+ // var fake_total = "fake_total";
+ // var fake_by_author = "fake_by_author";
+ //
+ // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
+ // spyOn(ContributorsStatGraphUtil, 'add');
+ // ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author);
+ // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]);
+ // });
+ // });
describe("#add", function () {
it("adds 1 to current test_field in collection", function () {
- var fake_collection = {test_field: 10}
- ContributorsStatGraphUtil.add(fake_collection, "test_field", 1)
- expect(fake_collection.test_field).toEqual(11)
- })
+ var fake_collection = { test_field: 10 };
+ ContributorsStatGraphUtil.add(fake_collection, "test_field", 1);
+ expect(fake_collection.test_field).toEqual(11);
+ });
it("inits and adds 1 if test_field in collection is not defined", function () {
- var fake_collection = {}
- ContributorsStatGraphUtil.add(fake_collection, "test_field", 1)
- expect(fake_collection.test_field).toEqual(1)
- })
- })
+ var fake_collection = {};
+ ContributorsStatGraphUtil.add(fake_collection, "test_field", 1);
+ expect(fake_collection.test_field).toEqual(1);
+ });
+ });
// TODO: fix or remove
- //describe("#store_additions", function () {
- //var fake_entry = {additions: 10}
- //var fake_total= "fake_total"
- //var fake_by_author = "fake_by_author"
- //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- //spyOn(ContributorsStatGraphUtil, 'add')
- //ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author)
- //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]])
- //})
- //})
+ // describe("#store_additions", function () {
+ // var fake_entry = {additions: 10};
+ // var fake_total= "fake_total";
+ // var fake_by_author = "fake_by_author";
+ // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
+ // spyOn(ContributorsStatGraphUtil, 'add');
+ // ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author);
+ // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]);
+ // });
+ // });
// TODO: fix or remove
- //describe("#store_deletions", function () {
- //var fake_entry = {deletions: 10}
- //var fake_total= "fake_total"
- //var fake_by_author = "fake_by_author"
- //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- //spyOn(ContributorsStatGraphUtil, 'add')
- //ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author)
- //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]])
- //})
- //})
+ // describe("#store_deletions", function () {
+ // var fake_entry = {deletions: 10};
+ // var fake_total= "fake_total";
+ // var fake_by_author = "fake_by_author";
+ // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
+ // spyOn(ContributorsStatGraphUtil, 'add');
+ // ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author);
+ // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]);
+ // });
+ // });
describe("#add_date", function () {
it("adds a date field to the collection", function () {
- var fake_date = "2013-10-02"
- var fake_collection = {}
- ContributorsStatGraphUtil.add_date(fake_date, fake_collection)
- expect(fake_collection[fake_date].date).toEqual("2013-10-02")
- })
- })
+ var fake_date = "2013-10-02";
+ var fake_collection = {};
+ ContributorsStatGraphUtil.add_date(fake_date, fake_collection);
+ expect(fake_collection[fake_date].date).toEqual("2013-10-02");
+ });
+ });
describe("#add_author", function () {
it("adds an author field to the collection", function () {
- var fake_author = { author_name: "Author", author_email: 'fake@email.com' }
- var fake_author_collection = {}
- var fake_email_collection = {}
- ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection)
- expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author")
- expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author")
- })
- })
+ var fake_author = { author_name: "Author", author_email: 'fake@email.com' };
+ var fake_author_collection = {};
+ var fake_email_collection = {};
+ ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection);
+ expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author");
+ expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author");
+ });
+ });
describe("#get_total_data", function () {
it("returns the collection sorted via specified field", function () {
var fake_parsed_log = {
- total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
- by_author:[
- {
- author: "Karlo Soriano",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
- },
- {
- author: "Dmitriy Zaporozhets",
- "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
- }
- ]};
- var correct_total_data = [{date: "2013-05-08", commits: 3},
- {date: "2013-05-09", commits: 1}];
- expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data)
- })
- })
+ total: [
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ ],
+ by_author: [
+ {
+ author: "Karlo Soriano",
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
+ },
+ {
+ author: "Dmitriy Zaporozhets",
+ "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ }
+ ]
+ };
+ var correct_total_data = [
+ { date: "2013-05-08", commits: 3 },
+ { date: "2013-05-09", commits: 1 }
+ ];
+ expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data);
+ });
+ });
describe("#pick_field", function () {
it("returns the collection with only the specified field and date", function () {
- var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}];
- ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")
- var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}];
- expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data)
- })
- })
+ var fake_parsed_log_total = [
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
+ ];
+ ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits");
+ var correct_pick_field_data = [{ date: "2013-05-09", commits: 1 }, { date: "2013-05-08", commits: 3 }];
+ expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data);
+ });
+ });
describe("#get_author_data", function () {
it("returns the log by author sorted by specified field", function () {
var fake_parsed_log = {
total: [
- {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
- {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
+ { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 },
+ { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
],
by_author: [
{
author_name: "Karlo Soriano", author_email: "karlo@email.com",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
},
{
author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com",
- "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
+ "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 }
}
]
- }
+ };
var correct_author_data = [
- {author_name:"Dmitriy Zaporozhets",author_email:"dzaporozhets@email.com",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3},
- {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}
- ]
- expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data)
- })
- })
+ { author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com", dates: { "2013-05-08": 3 }, deletions: 7, additions: 54, "commits": 3 },
+ { author_name: "Karlo Soriano", author_email: "karlo@email.com", dates: { "2013-05-09": 1 }, deletions: 0, additions: 471, commits: 1 }
+ ];
+ expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data);
+ });
+ });
describe("#parse_log_entry", function () {
it("adds the corresponding info from the log entry to the author", function () {
- var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com",
- "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
- }
- var correct_parsed_log = {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1}
- expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log)
- })
- })
+ var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com",
+ "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }
+ };
+ var correct_parsed_log = { author_name: "Karlo Soriano", author_email: "karlo@email.com", dates: { "2013-05-09": 1 }, deletions: 0, additions: 471, commits: 1 };
+ expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log);
+ });
+ });
describe("#in_range", function () {
- var date = "2013-05-09"
+ var date = "2013-05-09";
it("returns true if date_range is null", function () {
- expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true)
- })
+ expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true);
+ });
it("returns true if date is in range", function () {
- var date_range = [new Date("2013-01-01"), new Date("2013-12-12")]
- expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true)
- })
+ var date_range = [new Date("2013-01-01"), new Date("2013-12-12")];
+ expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true);
+ });
it("returns false if date is not in range", function () {
- var date_range = [new Date("1999-12-01"), new Date("2000-12-01")]
- expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false)
- })
- })
-
-
-})
+ var date_range = [new Date("1999-12-01"), new Date("2000-12-01")];
+ expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index 0da124632ae..71b589e6b83 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,10 +1,9 @@
-/* eslint-disable quotes, padded-blocks, semi */
+/* eslint-disable quotes */
/* global StatGraph */
//= require graphs/stat_graph
describe("StatGraph", function () {
-
describe("#get_log", function () {
it("returns log", function () {
StatGraph.log = "test";
@@ -16,7 +15,6 @@ describe("StatGraph", function () {
it("sets the log", function () {
StatGraph.set_log("test");
expect(StatGraph.log).toBe("test");
- })
- })
-
+ });
+ });
});
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index b5262afa1cf..b846c5ab00b 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,10 +1,9 @@
-/* eslint-disable space-before-function-paren, padded-blocks, no-var */
+/* eslint-disable space-before-function-paren, no-var */
/*= require header */
/*= require lib/utils/text_utility */
/*= require jquery */
(function() {
-
describe('Header', function() {
var todosPendingCount = '.todos-pending-count';
var fixtureTemplate = 'static/header.html.raw';
@@ -51,5 +50,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6
new file mode 100644
index 00000000000..92a20687ec5
--- /dev/null
+++ b/spec/javascripts/helpers/class_spec_helper.js.es6
@@ -0,0 +1,9 @@
+/* eslint-disable no-unused-vars */
+
+class ClassSpecHelper {
+ static itShouldBeAStaticMethod(base, method) {
+ return it('should be a static method', () => {
+ expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy();
+ });
+ }
+}
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js.es6
new file mode 100644
index 00000000000..d1155f1bd1e
--- /dev/null
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js.es6
@@ -0,0 +1,35 @@
+/* global ClassSpecHelper */
+//= require ./class_spec_helper
+
+describe('ClassSpecHelper', () => {
+ describe('.itShouldBeAStaticMethod', function () {
+ beforeEach(() => {
+ class TestClass {
+ instanceMethod() { this.prop = 'val'; }
+ static staticMethod() {}
+ }
+
+ this.TestClass = TestClass;
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod');
+
+ it('should have a defined spec', () => {
+ expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method');
+ });
+
+ it('should pass for a static method', () => {
+ const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod');
+ expect(spec.status()).toBe('passed');
+ });
+
+ it('should fail for an instance method', (done) => {
+ const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod');
+ spec.resultCallback = (result) => {
+ expect(result.status).toBe('failed');
+ done();
+ };
+ spec.execute();
+ });
+ });
+});
diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6
new file mode 100644
index 00000000000..a1e979e8d09
--- /dev/null
+++ b/spec/javascripts/issuable_time_tracker_spec.js.es6
@@ -0,0 +1,201 @@
+/* eslint-disable */
+//= require jquery
+//= require vue
+//= require issuable/time_tracking/components/time_tracker
+
+function initTimeTrackingComponent(opts) {
+ fixture.set(`
+ <div>
+ <div id="mock-container"></div>
+ </div>
+ `);
+
+ this.initialData = {
+ time_estimate: opts.timeEstimate,
+ time_spent: opts.timeSpent,
+ human_time_estimate: opts.timeEstimateHumanReadable,
+ human_time_spent: opts.timeSpentHumanReadable,
+ docsUrl: '/help/workflow/time_tracking.md',
+ };
+
+ const TimeTrackingComponent = Vue.component('issuable-time-tracker');
+ this.timeTracker = new TimeTrackingComponent({
+ el: '#mock-container',
+ propsData: this.initialData,
+ });
+}
+
+((gl) => {
+ describe('Issuable Time Tracker', function() {
+ describe('Initialization', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
+
+ it('should return something defined', function() {
+ expect(this.timeTracker).toBeDefined();
+ });
+
+ it ('should correctly set timeEstimate', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
+ done();
+ });
+ });
+ it ('should correctly set time_spent', function(done) {
+ Vue.nextTick(() => {
+ expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
+ done();
+ });
+ });
+ });
+
+ describe('Content Display', function() {
+ describe('Panes', function() {
+ describe('Comparison pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
+ });
+
+ it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
+ Vue.nextTick(() => {
+ const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
+ expect(this.timeTracker.showComparisonState).toBe(true);
+ done();
+ });
+ });
+
+ describe('Remaining meter', function() {
+ it('should display the remaining meter with the correct width', function(done) {
+ Vue.nextTick(() => {
+ const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
+ const correctWidth = '5%';
+
+ expect(meterWidth).toBe(correctWidth);
+ done();
+ })
+ });
+
+ it('should display the remaining meter with the correct background color when within estimate', function(done) {
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done()
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when over estimate', function(done) {
+ this.timeTracker.time_estimate = 100000;
+ this.timeTracker.time_spent = 20000000;
+ Vue.nextTick(() => {
+ const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
+ expect(styledMeter.length).toBe(1);
+ done();
+ });
+ });
+ });
+ });
+
+ describe("Estimate only pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
+ });
+
+ it('should display the human readable version of time estimated', function(done) {
+ Vue.nextTick(() => {
+ const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
+ const correctText = 'Estimated: 2h 46m';
+
+ expect(estimateText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Spent only pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
+ });
+
+ it('should display the human readable version of time spent', function(done) {
+ Vue.nextTick(() => {
+ const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
+ const correctText = 'Spent: 1h 23m';
+
+ expect(spentText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('No time tracking pane', function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 });
+ });
+
+ it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
+ Vue.nextTick(() => {
+ const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
+ const noTrackingText =$noTrackingPane.innerText;
+ const correctText = 'No estimate or time spent';
+
+ expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
+ expect($noTrackingPane).toBeVisible();
+ expect(noTrackingText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe("Help pane", function() {
+ beforeEach(function() {
+ initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
+ });
+
+ it('should not show the "Help" pane by default', function(done) {
+ Vue.nextTick(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
+ done();
+ });
+ });
+
+ it('should show the "Help" pane when help button is clicked', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
+
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+ expect(this.timeTracker.showHelpState).toBe(true);
+ expect($helpPane).toBeVisible();
+ done();
+ }, 10);
+ });
+ });
+
+ it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
+ Vue.nextTick(() => {
+ $(this.timeTracker.$el).find('.help-button').click();
+
+ setTimeout(() => {
+
+ $(this.timeTracker.$el).find('.close-help-button').click();
+
+ setTimeout(() => {
+ const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
+
+ expect(this.timeTracker.showHelpState).toBe(false);
+ expect($helpPane).toBeNull();
+
+ done();
+ }, 1000);
+ }, 1000);
+ });
+ });
+ });
+ });
+ });
+ });
+})(window.gl || (window.gl = {}));
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index eb07421826c..673a4b3c07a 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
/* global Issue */
/*= require lib/utils/text_utility */
@@ -42,21 +42,21 @@
}
function findElements() {
- $boxClosed = $('div.status-box-closed');
- expect($boxClosed).toExist();
- expect($boxClosed).toHaveText('Closed');
+ $boxClosed = $('div.status-box-closed');
+ expect($boxClosed).toExist();
+ expect($boxClosed).toHaveText('Closed');
- $boxOpen = $('div.status-box-open');
- expect($boxOpen).toExist();
- expect($boxOpen).toHaveText('Open');
+ $boxOpen = $('div.status-box-open');
+ expect($boxOpen).toExist();
+ expect($boxOpen).toHaveText('Open');
- $btnClose = $('.btn-close.btn-grouped');
- expect($btnClose).toExist();
- expect($btnClose).toHaveText('Close issue');
+ $btnClose = $('.btn-close.btn-grouped');
+ expect($btnClose).toExist();
+ expect($btnClose).toHaveText('Close issue');
- $btnReopen = $('.btn-reopen.btn-grouped');
- expect($btnReopen).toExist();
- expect($btnReopen).toHaveText('Reopen issue');
+ $btnReopen = $('.btn-reopen.btn-grouped');
+ expect($btnReopen).toExist();
+ expect($btnReopen).toHaveText('Reopen issue');
}
describe('Issue', function() {
@@ -161,5 +161,4 @@
expect($btnReopen).toHaveProp('disabled', false);
});
});
-
}).call(this);
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index e3146559a4a..0d19b4a25b9 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */
+/* eslint-disable no-new */
/* global IssuableContext */
/* global LabelsSelect */
@@ -26,18 +26,18 @@
spyOn(jQuery, 'ajax').and.callFake((req) => {
const d = $.Deferred();
- let LABELS_DATA = []
+ let LABELS_DATA = [];
if (req.url === '/root/test/labels.json') {
- for (let i = 0; i < 10; i++) {
- LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ for (let i = 0; i < 10; i += 1) {
+ LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
} else if (req.url === '/root/test/issues/2.json') {
- let tmp = []
- for (let i = 0; i < saveLabelCount; i++) {
- tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ const tmp = [];
+ for (let i = 0; i < saveLabelCount; i += 1) {
+ tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
- LABELS_DATA = {labels: tmp};
+ LABELS_DATA = { labels: tmp };
}
d.resolve(LABELS_DATA);
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
index 031f9ca03c9..1ce8f28e568 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -52,5 +52,22 @@
expect(value).toBe(null);
});
});
+
+ describe('gl.utils.normalizedHeaders', () => {
+ it('should upperCase all the header keys to keep them consistent', () => {
+ const apiHeaders = {
+ 'X-Something-Workhorse': { workhorse: 'ok' },
+ 'x-something-nginx': { nginx: 'ok' },
+ };
+
+ const normalized = gl.utils.normalizeHeaders(apiHeaders);
+
+ const WORKHORSE = 'X-SOMETHING-WORKHORSE';
+ const NGINX = 'X-SOMETHING-NGINX';
+
+ expect(normalized[WORKHORSE].workhorse).toBe('ok');
+ expect(normalized[NGINX].nginx).toBe('ok');
+ });
+ });
});
})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index 31f516b41bf..6605986c33a 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */
/* global LineHighlighter */
/*= require line_highlighter */
@@ -33,11 +33,11 @@
return expect($('#LC13')).toHaveClass(this.css);
});
it('highlights a range of lines given in the URL hash', function() {
- var i, line, results;
+ var line, results;
new LineHighlighter('#L5-25');
expect($("." + this.css).length).toBe(21);
results = [];
- for (line = i = 5; i <= 25; line = ++i) {
+ for (line = 5; line <= 25; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
@@ -124,27 +124,27 @@
});
describe('with existing single-line highlight', function() {
it('uses existing line as last line when target is lesser', function() {
- var i, line, results;
+ var line, results;
clickLine(20);
clickLine(15, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 15; i <= 20; line = ++i) {
+ for (line = 15; line <= 20; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
});
return it('uses existing line as first line when target is greater', function() {
- var i, line, results;
+ var line, results;
clickLine(5);
clickLine(10, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 5; i <= 10; line = ++i) {
+ for (line = 5; line <= 10; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
@@ -160,25 +160,25 @@
});
});
it('uses target as first line when it is less than existing first line', function() {
- var i, line, results;
+ var line, results;
clickLine(5, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 5; i <= 10; line = ++i) {
+ for (line = 5; line <= 10; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
});
return it('uses target as last line when it is greater than existing first line', function() {
- var i, line, results;
+ var line, results;
clickLine(15, {
shiftKey: true
});
expect($("." + this.css).length).toBe(6);
results = [];
- for (line = i = 10; i <= 15; line = ++i) {
+ for (line = 10; line <= 15; line += 1) {
results.push(expect($("#LC" + line)).toHaveClass(this.css));
}
return results;
@@ -227,5 +227,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 9b232617fe5..f644d39b1c7 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */
+/* eslint-disable space-before-function-paren, no-return-assign */
/* global MergeRequest */
/*= require merge_request */
@@ -26,5 +26,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 6f91529db00..bf45100af03 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */
/*= require merge_request_widget */
/*= require lib/utils/datetime_utility */
@@ -42,17 +42,17 @@
});
it('should call renderEnvironments when the environments property is set', function() {
- const spy = spyOn(this.class, 'renderEnvironments').and.stub();
- this.class.getCIEnvironmentsStatus();
- expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
- });
-
- it('should not call renderEnvironments when the environments property is not set', function() {
- this.ciEnvironmentsStatusData = null;
- const spy = spyOn(this.class, 'renderEnvironments').and.stub();
- this.class.getCIEnvironmentsStatus();
- expect(spy).not.toHaveBeenCalled();
- });
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
+ });
+
+ it('should not call renderEnvironments when the environments property is not set', function() {
+ this.ciEnvironmentsStatusData = null;
+ const spy = spyOn(this.class, 'renderEnvironments').and.stub();
+ this.class.getCIEnvironmentsStatus();
+ expect(spy).not.toHaveBeenCalled();
+ });
});
describe('renderEnvironments', function() {
@@ -107,16 +107,16 @@
});
describe('mergeInProgress', function() {
- it('should display error with h4 tag', function() {
- spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
- expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
- });
- spyOn($, 'ajax').and.callFake(function(e) {
- e.success({ merge_error: 'Sorry, something went wrong.' });
- });
- this.class.mergeInProgress(null);
+ it('should display error with h4 tag', function() {
+ spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
+ expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
+ });
+ spyOn($, 'ajax').and.callFake(function(e) {
+ e.success({ merge_error: 'Sorry, something went wrong.' });
});
+ this.class.mergeInProgress(null);
});
+ });
return describe('getCIStatus', function() {
beforeEach(function() {
@@ -167,5 +167,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index e0dc549a9f4..8259d553f1b 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
/* global NewBranchForm */
/*= require jquery-ui/autocomplete */
@@ -166,5 +166,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 9cdb0a5d5aa..015c35dfca7 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
/*= require notes */
@@ -72,8 +72,7 @@
$('.js-comment-button').click();
expect(this.autoSizeSpy).toHaveBeenTriggered();
- })
+ });
});
});
-
}).call(this);
diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6
index 2e12d45f7a7..7a04fba5f7f 100644
--- a/spec/javascripts/pretty_time_spec.js.es6
+++ b/spec/javascripts/pretty_time_spec.js.es6
@@ -1,12 +1,12 @@
//= require lib/utils/pretty_time
(() => {
- const PrettyTime = gl.PrettyTime;
+ const prettyTime = gl.utils.prettyTime;
- describe('PrettyTime methods', function () {
+ describe('prettyTime methods', function () {
describe('parseSeconds', function () {
it('should correctly parse a negative value', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const zeroSeconds = parser(-1000);
@@ -17,7 +17,7 @@
});
it('should correctly parse a zero value', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const zeroSeconds = parser(0);
@@ -28,7 +28,7 @@
});
it('should correctly parse a small non-zero second values', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const subOneMinute = parser(10);
@@ -53,7 +53,7 @@
});
it('should correctly parse large second values', function () {
- const parser = PrettyTime.parseSeconds;
+ const parser = prettyTime.parseSeconds;
const aboveOneHour = parser(4800);
@@ -87,7 +87,7 @@
minutes: 20,
};
- const timeString = PrettyTime.stringifyTime(timeObject);
+ const timeString = prettyTime.stringifyTime(timeObject);
expect(timeString).toBe('1w 4d 7h 20m');
});
@@ -100,7 +100,7 @@
minutes: 20,
};
- const timeString = PrettyTime.stringifyTime(timeObject);
+ const timeString = prettyTime.stringifyTime(timeObject);
expect(timeString).toBe('4d 20m');
});
@@ -113,7 +113,7 @@
minutes: 0,
};
- const timeString = PrettyTime.stringifyTime(timeObject);
+ const timeString = prettyTime.stringifyTime(timeObject);
expect(timeString).toBe('0m');
});
@@ -122,12 +122,12 @@
describe('abbreviateTime', function () {
it('should abbreviate stringified times for weeks', function () {
const fullTimeString = '1w 3d 4h 5m';
- expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('1w');
+ expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w');
});
it('should abbreviate stringified times for non-weeks', function () {
const fullTimeString = '0w 3d 4h 5m';
- expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('3d');
+ expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d');
});
});
});
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 27b071f266d..0202c9ba85e 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */
/* global Project */
@@ -49,5 +49,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 0177d8e4e79..942778229b5 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
/* global Sidebar */
/*= require right_sidebar */
@@ -78,7 +78,6 @@
$('.js-issuable-todo').click();
expect(todoToggleSpy.calls.count()).toEqual(1);
- })
+ });
});
-
}).call(this);
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 2d3f44e7980..7ac9710654f 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */
/*= require gl_dropdown */
/*= require search_autocomplete */
@@ -171,5 +171,4 @@
expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
});
});
-
}).call(this);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index ae5d639ad9c..755dba19744 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */
+/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
/* global ShortcutsIssuable */
/*= require shortcuts_issuable */
@@ -75,5 +75,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6
index 6a70dd856a7..99f45850ea3 100644
--- a/spec/javascripts/subbable_resource_spec.js.es6
+++ b/spec/javascripts/subbable_resource_spec.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */
+/* eslint-disable max-len, arrow-parens, comma-dangle */
//= vue
//= vue-resource
@@ -53,7 +53,7 @@
this.MockResource.subscribe(callbacks.two);
this.MockResource.subscribe(callbacks.three);
- state.myprop++;
+ state.myprop += 1;
this.MockResource.publish(state);
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 5984ce8ffd4..436f7064a69 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
/*= require syntax_highlight */
@@ -41,5 +41,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index dc2f4967985..80163fd72d3 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, max-len */
/* global MockU2FDevice */
/* global U2FAuthenticate */
@@ -69,5 +69,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 1459f968c3d..287bfb4138b 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,6 +1,7 @@
-/* eslint-disable space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, max-len */
+
(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+ var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.MockU2FDevice = (function() {
function MockU2FDevice() {
@@ -28,7 +29,5 @@
};
return MockU2FDevice;
-
})();
-
}).call(this);
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index ab4c5edd044..0790553b67e 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, max-len */
/* global MockU2FDevice */
/* global U2FRegister */
@@ -74,5 +74,4 @@
});
});
});
-
}).call(this);
diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js.es6
new file mode 100644
index 00000000000..b21f6912e06
--- /dev/null
+++ b/spec/javascripts/visibility_select_spec.js.es6
@@ -0,0 +1,100 @@
+/*= require visibility_select */
+
+(() => {
+ const VisibilitySelect = gl.VisibilitySelect;
+
+ describe('VisibilitySelect', function () {
+ const lockedElement = document.createElement('div');
+ lockedElement.dataset.helpBlock = 'lockedHelpBlock';
+
+ const checkedElement = document.createElement('div');
+ checkedElement.dataset.description = 'checkedDescription';
+
+ const mockElements = {
+ container: document.createElement('div'),
+ select: document.createElement('div'),
+ '.help-block': document.createElement('div'),
+ '.js-locked': lockedElement,
+ 'option:checked': checkedElement,
+ };
+
+ beforeEach(function () {
+ spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]);
+ });
+
+ describe('#constructor', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ });
+
+ it('sets the container member', function () {
+ expect(this.visibilitySelect.container).toEqual(mockElements.container);
+ });
+
+ it('queries and sets the helpBlock member', function () {
+ expect(Element.prototype.querySelector).toHaveBeenCalledWith('.help-block');
+ expect(this.visibilitySelect.helpBlock).toEqual(mockElements['.help-block']);
+ });
+
+ it('queries and sets the select member', function () {
+ expect(Element.prototype.querySelector).toHaveBeenCalledWith('select');
+ expect(this.visibilitySelect.select).toEqual(mockElements.select);
+ });
+
+ describe('if there is no container element provided', function () {
+ it('throws an error', function () {
+ expect(() => new VisibilitySelect()).toThrowError('VisibilitySelect requires a container element as argument 1');
+ });
+ });
+ });
+
+ describe('#init', function () {
+ describe('if there is a select', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ });
+
+ it('calls updateHelpText', function () {
+ spyOn(VisibilitySelect.prototype, 'updateHelpText');
+ this.visibilitySelect.init();
+ expect(this.visibilitySelect.updateHelpText).toHaveBeenCalled();
+ });
+
+ it('adds a change event listener', function () {
+ spyOn(this.visibilitySelect.select, 'addEventListener');
+ this.visibilitySelect.init();
+ expect(this.visibilitySelect.select.addEventListener.calls.argsFor(0)).toContain('change');
+ });
+ });
+
+ describe('if there is no select', function () {
+ beforeEach(function () {
+ mockElements.select = undefined;
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ this.visibilitySelect.init();
+ });
+
+ it('updates the helpBlock text to the locked `data-help-block` messaged', function () {
+ expect(this.visibilitySelect.helpBlock.textContent)
+ .toEqual(lockedElement.dataset.helpBlock);
+ });
+
+ afterEach(function () {
+ mockElements.select = document.createElement('div');
+ });
+ });
+ });
+
+ describe('#updateHelpText', function () {
+ beforeEach(function () {
+ this.visibilitySelect = new VisibilitySelect(mockElements.container);
+ this.visibilitySelect.init();
+ });
+
+ it('updates the helpBlock text to the selected options `data-description`', function () {
+ expect(this.visibilitySelect.helpBlock.textContent)
+ .toEqual(checkedElement.dataset.description);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index f1c2edcc55c..be706ca304f 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, max-len */
/* global Dropzone */
/* global Mousetrap */
/* global ZenMode */
@@ -76,5 +76,4 @@
keyCode: 27
}));
};
-
}).call(this);
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 5bfeb82e738..3e1ac9fb2b2 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -152,6 +152,30 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
end
+ context 'when a project is not specified' do
+ let(:project) { nil }
+
+ it 'does not link a User' do
+ doc = reference_filter("Hey #{reference}")
+
+ expect(doc).not_to include('a')
+ end
+
+ context 'when skip_project_check set to true' do
+ it 'links to a User' do
+ doc = reference_filter("Hey #{reference}", skip_project_check: true)
+
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ it 'does not link users using @all reference' do
+ doc = reference_filter("Hey #{User.reference_prefix}all", skip_project_check: true)
+
+ expect(doc).not_to include('a')
+ end
+ end
+ end
+
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index dccb29b5ef6..0c40fca0c1a 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -3,15 +3,23 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Factory do
let(:user) { create(:user) }
let(:project) { build.project }
-
- subject { described_class.new(build, user) }
- let(:status) { subject.fabricate! }
+ let(:status) { factory.fabricate! }
+ let(:factory) { described_class.new(build, user) }
before { project.team << [user, :developer] }
context 'when build is successful' do
let(:build) { create(:ci_build, :success) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable]
+ end
+
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
@@ -26,24 +34,72 @@ describe Gitlab::Ci::Status::Build::Factory do
end
context 'when build is failed' do
- let(:build) { create(:ci_build, :failed) }
+ context 'when build is not allowed to fail' do
+ let(:build) { create(:ci_build, :failed) }
- it 'fabricates a retryable build status' do
- expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable]
+ end
+
+ it 'fabricates a retryable build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'icon_status_failed'
+ expect(status.label).to eq 'failed'
+ expect(status).to have_details
+ expect(status).to have_action
+ end
end
- it 'fabricates status with correct details' do
- expect(status.text).to eq 'failed'
- expect(status.icon).to eq 'icon_status_failed'
- expect(status.label).to eq 'failed'
- expect(status).to have_details
- expect(status).to have_action
+ context 'when build is allowed to fail' do
+ let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable,
+ Gitlab::Ci::Status::Build::FailedAllowed]
+ end
+
+ it 'fabricates a failed but allowed build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::FailedAllowed
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'icon_status_warning'
+ expect(status.label).to eq 'failed (allowed to fail)'
+ expect(status).to have_details
+ expect(status).to have_action
+ expect(status.action_title).to include 'Retry'
+ expect(status.action_path).to include 'retry'
+ end
end
end
context 'when build is a canceled' do
let(:build) { create(:ci_build, :canceled) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Canceled
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable]
+ end
+
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
@@ -60,6 +116,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is running' do
let(:build) { create(:ci_build, :running) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Running
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Cancelable]
+ end
+
it 'fabricates a canceable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end
@@ -76,6 +141,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is pending' do
let(:build) { create(:ci_build, :pending) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Pending
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Cancelable]
+ end
+
it 'fabricates a cancelable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end
@@ -92,6 +166,14 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is skipped' do
let(:build) { create(:ci_build, :skipped) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'does not match extended statuses' do
+ expect(factory.extended_statuses).to be_empty
+ end
+
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Skipped
end
@@ -109,6 +191,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is a play action' do
let(:build) { create(:ci_build, :playable) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Play]
+ end
+
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Play
end
@@ -119,12 +210,22 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.label).to eq 'manual play action'
expect(status).to have_details
expect(status).to have_action
+ expect(status.action_path).to include 'play'
end
end
context 'when build is an environment stop action' do
let(:build) { create(:ci_build, :playable, :teardown_environment) }
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Stop]
+ end
+
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Stop
end
diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
new file mode 100644
index 00000000000..20f71459738
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::FailedAllowed do
+ let(:status) { double('core status') }
+ let(:user) { double('user') }
+
+ subject do
+ described_class.new(status)
+ end
+
+ describe '#text' do
+ it 'does not override status text' do
+ expect(status).to receive(:text)
+
+ subject.text
+ end
+ end
+
+ describe '#icon' do
+ it 'returns a warning icon' do
+ expect(subject.icon).to eq 'icon_status_warning'
+ end
+ end
+
+ describe '#label' do
+ it 'returns information about failed but allowed to fail status' do
+ expect(subject.label).to eq 'failed (allowed to fail)'
+ end
+ end
+
+ describe '#group' do
+ it 'returns status failed with warnings status group' do
+ expect(subject.group).to eq 'failed_with_warnings'
+ end
+ end
+
+ describe 'action details' do
+ describe '#has_action?' do
+ it 'does not decorate action details' do
+ expect(status).to receive(:has_action?)
+
+ subject.has_action?
+ end
+ end
+
+ describe '#action_path' do
+ it 'does not decorate action path' do
+ expect(status).to receive(:action_path)
+
+ subject.action_path
+ end
+ end
+
+ describe '#action_icon' do
+ it 'does not decorate action icon' do
+ expect(status).to receive(:action_icon)
+
+ subject.action_icon
+ end
+ end
+
+ describe '#action_title' do
+ it 'does not decorate action title' do
+ expect(status).to receive(:action_title)
+
+ subject.action_title
+ end
+ end
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, user) }
+
+ context 'when build is failed' do
+ context 'when build is allowed to fail' do
+ let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not allowed to fail' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it 'is not a correct match' do
+ expect(subject).not_to be true
+ end
+ end
+ end
+
+ context 'when build did not fail' do
+ context 'when build is allowed to fail' do
+ let(:build) { create(:ci_build, :success, :allowed_to_fail) }
+
+ it 'is not a correct match' do
+ expect(subject).not_to be true
+ end
+ end
+
+ context 'when build is not allowed to fail' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'is not a correct match' do
+ expect(subject).not_to be true
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb
new file mode 100644
index 00000000000..5a97d98b55f
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/external/common_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::External::Common do
+ let(:user) { create(:user) }
+ let(:project) { external_status.project }
+ let(:external_target_url) { 'http://example.gitlab.com/status' }
+
+ let(:external_status) do
+ create(:generic_commit_status, target_url: external_target_url)
+ end
+
+ subject do
+ Gitlab::Ci::Status::Core
+ .new(external_status, user)
+ .extend(described_class)
+ end
+
+ describe '#has_action?' do
+ it { is_expected.not_to have_action }
+ end
+
+ describe '#has_details?' do
+ context 'when user has access to read commit status' do
+ before { project.team << [user, :developer] }
+
+ it { is_expected.to have_details }
+ end
+
+ context 'when user does not have access to read commit status' do
+ it { is_expected.not_to have_details }
+ end
+ end
+
+ describe '#details_path' do
+ it 'links to the external target URL' do
+ expect(subject.details_path).to eq external_target_url
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb
new file mode 100644
index 00000000000..c96fd53e730
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::External::Factory do
+ let(:user) { create(:user) }
+ let(:project) { resource.project }
+ let(:status) { factory.fabricate! }
+ let(:factory) { described_class.new(resource, user) }
+ let(:external_url) { 'http://gitlab.com/status' }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when external status has a simple core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+ context "when core status is #{simple_status}" do
+ let(:resource) do
+ create(:generic_commit_status, status: simple_status,
+ target_url: external_url)
+ end
+
+ let(:expected_status) do
+ Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ end
+
+ it "fabricates a core status #{simple_status}" do
+ expect(status).to be_a expected_status
+ end
+
+ it 'extends core status with common methods' do
+ expect(status).to have_details
+ expect(status).not_to have_action
+ expect(status.details_path).to eq external_url
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/factory_spec.rb b/spec/lib/gitlab/ci/status/factory_spec.rb
index f92a1c149bf..bbf9c7c83a3 100644
--- a/spec/lib/gitlab/ci/status/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/factory_spec.rb
@@ -1,24 +1,135 @@
require 'spec_helper'
describe Gitlab::Ci::Status::Factory do
- subject do
- described_class.new(resource, user)
+ let(:user) { create(:user) }
+ let(:fabricated_status) { factory.fabricate! }
+ let(:factory) { described_class.new(resource, user) }
+
+ context 'when object has a core status' do
+ HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+ context "when simple core status is #{simple_status}" do
+ let(:resource) { double('resource', status: simple_status) }
+
+ let(:expected_status) do
+ Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ end
+
+ it "fabricates a core status #{simple_status}" do
+ expect(fabricated_status).to be_a expected_status
+ end
+
+ it "matches a valid core status for #{simple_status}" do
+ expect(factory.core_status).to be_a expected_status
+ end
+
+ it "does not match any extended statuses for #{simple_status}" do
+ expect(factory.extended_statuses).to be_empty
+ end
+ end
+ end
end
- let(:user) { create(:user) }
+ context 'when resource supports multiple extended statuses' do
+ let(:resource) { double('resource', status: :success) }
- let(:status) { subject.fabricate! }
+ let(:first_extended_status) do
+ Class.new(SimpleDelegator) do
+ def first_method
+ 'first return value'
+ end
- context 'when object has a core status' do
- HasStatus::AVAILABLE_STATUSES.each do |core_status|
- context "when core status is #{core_status}" do
- let(:resource) { double(status: core_status) }
+ def second_method
+ 'second return value'
+ end
+
+ def self.matches?(*)
+ true
+ end
+ end
+ end
- it "fabricates a core status #{core_status}" do
- expect(status).to be_a(
- Gitlab::Ci::Status.const_get(core_status.capitalize))
+ let(:second_extended_status) do
+ Class.new(SimpleDelegator) do
+ def first_method
+ 'decorated return value'
end
+
+ def third_method
+ 'third return value'
+ end
+
+ def self.matches?(*)
+ true
+ end
+ end
+ end
+
+ shared_examples 'compound decorator factory' do
+ it 'fabricates compound decorator' do
+ expect(fabricated_status.first_method).to eq 'decorated return value'
+ expect(fabricated_status.second_method).to eq 'second return value'
+ expect(fabricated_status.third_method).to eq 'third return value'
end
+
+ it 'delegates to core status' do
+ expect(fabricated_status.text).to eq 'passed'
+ end
+
+ it 'latest matches status becomes a status name' do
+ expect(fabricated_status.class).to eq second_extended_status
+ end
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [first_extended_status, second_extended_status]
+ end
+ end
+
+ context 'when exclusive statuses are matches' do
+ before do
+ allow(described_class).to receive(:extended_statuses)
+ .and_return([[first_extended_status, second_extended_status]])
+ end
+
+ it 'does not fabricate compound decorator' do
+ expect(fabricated_status.first_method).to eq 'first return value'
+ expect(fabricated_status.second_method).to eq 'second return value'
+ expect(fabricated_status).not_to respond_to(:third_method)
+ end
+
+ it 'delegates to core status' do
+ expect(fabricated_status.text).to eq 'passed'
+ end
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses).to eq [first_extended_status]
+ end
+ end
+
+ context 'when exclusive statuses are not matched' do
+ before do
+ allow(described_class).to receive(:extended_statuses)
+ .and_return([[first_extended_status], [second_extended_status]])
+ end
+
+ it_behaves_like 'compound decorator factory'
+ end
+
+ context 'when using simplified status grouping' do
+ before do
+ allow(described_class).to receive(:extended_statuses)
+ .and_return([first_extended_status, second_extended_status])
+ end
+
+ it_behaves_like 'compound decorator factory'
end
end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
index d4a2dc7fcc1..b10a447c27a 100644
--- a/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/pipeline/factory_spec.rb
@@ -3,29 +3,32 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Factory do
let(:user) { create(:user) }
let(:project) { pipeline.project }
-
- subject do
- described_class.new(pipeline, user)
- end
-
- let(:status) do
- subject.fabricate!
- end
+ let(:status) { factory.fabricate! }
+ let(:factory) { described_class.new(pipeline, user) }
before do
project.team << [user, :developer]
end
context 'when pipeline has a core status' do
- HasStatus::AVAILABLE_STATUSES.each do |core_status|
- context "when core status is #{core_status}" do
- let(:pipeline) do
- create(:ci_pipeline, status: core_status)
+ HasStatus::AVAILABLE_STATUSES.each do |simple_status|
+ context "when core status is #{simple_status}" do
+ let(:pipeline) { create(:ci_pipeline, status: simple_status) }
+
+ let(:expected_status) do
+ Gitlab::Ci::Status.const_get(simple_status.capitalize)
+ end
+
+ it "matches correct core status for #{simple_status}" do
+ expect(factory.core_status).to be_a expected_status
end
- it "fabricates a core status #{core_status}" do
- expect(status).to be_a(
- Gitlab::Ci::Status.const_get(core_status.capitalize))
+ it 'does not matche extended statuses' do
+ expect(factory.extended_statuses).to be_empty
+ end
+
+ it "fabricates a core status #{simple_status}" do
+ expect(status).to be_a expected_status
end
it 'extends core status with common pipeline methods' do
@@ -47,13 +50,22 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline)
end
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::SuccessWarning]
+ end
+
it 'fabricates extended "success with warnings" status' do
- expect(status)
- .to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings
+ expect(status).to be_a Gitlab::Ci::Status::SuccessWarning
end
- it 'extends core status with common pipeline methods' do
+ it 'extends core status with common pipeline method' do
expect(status).to have_details
+ expect(status.details_path).to include "pipelines/#{pipeline.id}"
end
end
end
diff --git a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb b/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
deleted file mode 100644
index 979160eb9c4..00000000000
--- a/spec/lib/gitlab/ci/status/pipeline/success_with_warnings_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
- subject do
- described_class.new(double('status'))
- end
-
- describe '#test' do
- it { expect(subject.text).to eq 'passed' }
- end
-
- describe '#label' do
- it { expect(subject.label).to eq 'passed with warnings' }
- end
-
- describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_warning' }
- end
-
- describe '#group' do
- it { expect(subject.group).to eq 'success_with_warnings' }
- end
-
- describe '.matches?' do
- context 'when pipeline is successful' do
- let(:pipeline) do
- create(:ci_pipeline, status: :success)
- end
-
- context 'when pipeline has warnings' do
- before do
- allow(pipeline).to receive(:has_warnings?).and_return(true)
- end
-
- it 'is a correct match' do
- expect(described_class.matches?(pipeline, double)).to eq true
- end
- end
-
- context 'when pipeline does not have warnings' do
- it 'does not match' do
- expect(described_class.matches?(pipeline, double)).to eq false
- end
- end
- end
-
- context 'when pipeline is not successful' do
- let(:pipeline) do
- create(:ci_pipeline, status: :skipped)
- end
-
- context 'when pipeline has warnings' do
- before do
- allow(pipeline).to receive(:has_warnings?).and_return(true)
- end
-
- it 'does not match' do
- expect(described_class.matches?(pipeline, double)).to eq false
- end
- end
-
- context 'when pipeline does not have warnings' do
- it 'does not match' do
- expect(described_class.matches?(pipeline, double)).to eq false
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index 6f8721d30c2..bbb40e2c1ab 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -43,4 +43,25 @@ describe Gitlab::Ci::Status::Stage::Factory do
end
end
end
+
+ context 'when stage has warnings' do
+ let(:stage) do
+ build(:ci_stage, name: 'test', status: :success, pipeline: pipeline)
+ end
+
+ before do
+ create(:ci_build, :allowed_to_fail, :failed,
+ stage: 'test', pipeline: stage.pipeline)
+ end
+
+ it 'fabricates extended "success with warnings" status' do
+ expect(status)
+ .to be_a Gitlab::Ci::Status::SuccessWarning
+ end
+
+ it 'extends core status with common stage method' do
+ expect(status).to have_details
+ expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}"
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
new file mode 100644
index 00000000000..7e2269397c6
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::SuccessWarning do
+ subject do
+ described_class.new(double('status'))
+ end
+
+ describe '#test' do
+ it { expect(subject.text).to eq 'passed' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'passed with warnings' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_warning' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'success_with_warnings' }
+ end
+
+ describe '.matches?' do
+ let(:matchable) { double('matchable') }
+
+ context 'when matchable subject is successful' do
+ before do
+ allow(matchable).to receive(:success?).and_return(true)
+ end
+
+ context 'when matchable subject has warnings' do
+ before do
+ allow(matchable).to receive(:has_warnings?).and_return(true)
+ end
+
+ it 'is a correct match' do
+ expect(described_class.matches?(matchable, double)).to eq true
+ end
+ end
+
+ context 'when matchable subject does not have warnings' do
+ before do
+ allow(matchable).to receive(:has_warnings?).and_return(false)
+ end
+
+ it 'does not match' do
+ expect(described_class.matches?(matchable, double)).to eq false
+ end
+ end
+ end
+
+ context 'when matchable subject is not successful' do
+ before do
+ allow(matchable).to receive(:success?).and_return(false)
+ end
+
+ context 'when matchable subject has warnings' do
+ before do
+ allow(matchable).to receive(:has_warnings?).and_return(true)
+ end
+
+ it 'does not match' do
+ expect(described_class.matches?(matchable, double)).to eq false
+ end
+ end
+
+ context 'when matchable subject does not have warnings' do
+ it 'does not match' do
+ expect(described_class.matches?(matchable, double)).to eq false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 004341ffd02..b01c4805a34 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -1,36 +1,64 @@
require 'spec_helper'
describe Gitlab::CurrentSettings do
+ include StubENV
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
describe '#current_application_settings' do
- it 'attempts to use cached values first' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
- expect(ApplicationSetting).not_to receive(:last)
+ context 'with DB available' do
+ before do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
+ end
- expect(current_application_settings).to be_a(ApplicationSetting)
- end
+ it 'attempts to use cached values first' do
+ expect(ApplicationSetting).to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
- it 'does not attempt to connect to DB or Redis' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
- expect(ApplicationSetting).not_to receive(:current)
- expect(ApplicationSetting).not_to receive(:last)
+ it 'falls back to DB if Redis returns an empty value' do
+ expect(ApplicationSetting).to receive(:last).and_call_original
- expect(current_application_settings).to eq fake_application_settings
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
+
+ it 'falls back to DB if Redis fails' do
+ expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
+ expect(ApplicationSetting).to receive(:last).and_call_original
+
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ end
end
- it 'falls back to DB if Redis returns an empty value' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ context 'with DB unavailable' do
+ before do
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(false)
+ end
- expect(current_application_settings).to be_a(ApplicationSetting)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
+
+ expect(current_application_settings).to be_a(OpenStruct)
+ end
end
- it 'falls back to DB if Redis fails' do
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:connect_to_db?).and_return(true)
- expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError)
- expect(ApplicationSetting).to receive(:last).and_call_original
+ context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is true' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
+ end
+
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).not_to receive(:current)
+ expect(ApplicationSetting).not_to receive(:last)
- expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(current_application_settings).not_to be_persisted
+ end
end
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb
new file mode 100644
index 00000000000..0267e8c2f69
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::CodeEventFetcher do
+ let(:stage_name) { :code }
+
+ it_behaves_like 'default query config' do
+ it 'has a default order' do
+ expect(event.order).not_to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
deleted file mode 100644
index 43f42d1bde8..00000000000
--- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::CodeEvent do
- it_behaves_like 'default query config' do
- it 'does not have the default order' do
- expect(event.order).not_to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
new file mode 100644
index 00000000000..e8fc67acf05
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::CodeStage do
+ let(:stage_name) { :code }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index 6062e7af4f5..9d2ba481919 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
-describe Gitlab::CycleAnalytics::Events do
+describe 'cycle analytics events' do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
- subject { described_class.new(project: project, options: { from: from_date, current_user: user }) }
+ let(:events) do
+ CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events
+ end
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
@@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do
end
describe '#issue_events' do
+ let(:stage) { :issue }
+
it 'has the total time' do
- expect(subject.issue_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.issue_events.first[:title]).to eq(context.title)
+ expect(events.first[:title]).to eq(context.title)
end
it 'has the URL' do
- expect(subject.issue_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has an iid' do
- expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has a created_at timestamp' do
- expect(subject.issue_events.first[:created_at]).to end_with('ago')
+ expect(events.first[:created_at]).to end_with('ago')
end
it "has the author's URL" do
- expect(subject.issue_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.issue_events.first[:author][:name]).to eq(context.author.name)
+ expect(events.first[:author][:name]).to eq(context.author.name)
end
end
describe '#plan_events' do
+ let(:stage) { :plan }
+
it 'has a title' do
- expect(subject.plan_events.first[:title]).not_to be_nil
+ expect(events.first[:title]).not_to be_nil
end
it 'has a sha short ID' do
- expect(subject.plan_events.first[:short_sha]).not_to be_nil
+ expect(events.first[:short_sha]).not_to be_nil
end
it 'has the URL' do
- expect(subject.plan_events.first[:commit_url]).not_to be_nil
+ expect(events.first[:commit_url]).not_to be_nil
end
it 'has the total time' do
- expect(subject.plan_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it "has the author's URL" do
- expect(subject.plan_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.plan_events.first[:author][:name]).not_to be_nil
+ expect(events.first[:author][:name]).not_to be_nil
end
end
describe '#code_events' do
+ let(:stage) { :code }
+
before do
create_commit_referencing_issue(context)
end
it 'has the total time' do
- expect(subject.code_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.code_events.first[:title]).to eq('Awesome merge_request')
+ expect(events.first[:title]).to eq('Awesome merge_request')
end
it 'has an iid' do
- expect(subject.code_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has a created_at timestamp' do
- expect(subject.code_events.first[:created_at]).to end_with('ago')
+ expect(events.first[:created_at]).to end_with('ago')
end
it "has the author's URL" do
- expect(subject.code_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
describe '#test_events' do
+ let(:stage) { :test }
+
let(:merge_request) { MergeRequest.first }
let!(:pipeline) do
create(:ci_pipeline,
@@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do
end
it 'has the name' do
- expect(subject.test_events.first[:name]).not_to be_nil
+ expect(events.first[:name]).not_to be_nil
end
it 'has the ID' do
- expect(subject.test_events.first[:id]).not_to be_nil
+ expect(events.first[:id]).not_to be_nil
end
it 'has the URL' do
- expect(subject.test_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has the branch name' do
- expect(subject.test_events.first[:branch]).not_to be_nil
+ expect(events.first[:branch]).not_to be_nil
end
it 'has the branch URL' do
- expect(subject.test_events.first[:branch][:url]).not_to be_nil
+ expect(events.first[:branch][:url]).not_to be_nil
end
it 'has the short SHA' do
- expect(subject.test_events.first[:short_sha]).not_to be_nil
+ expect(events.first[:short_sha]).not_to be_nil
end
it 'has the commit URL' do
- expect(subject.test_events.first[:commit_url]).not_to be_nil
+ expect(events.first[:commit_url]).not_to be_nil
end
it 'has the date' do
- expect(subject.test_events.first[:date]).not_to be_nil
+ expect(events.first[:date]).not_to be_nil
end
it 'has the total time' do
- expect(subject.test_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
end
describe '#review_events' do
+ let(:stage) { :review }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
it 'has the total time' do
- expect(subject.review_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.review_events.first[:title]).to eq('Awesome merge_request')
+ expect(events.first[:title]).to eq('Awesome merge_request')
end
it 'has an iid' do
- expect(subject.review_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has the URL' do
- expect(subject.review_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has a state' do
- expect(subject.review_events.first[:state]).not_to be_nil
+ expect(events.first[:state]).not_to be_nil
end
it 'has a created_at timestamp' do
- expect(subject.review_events.first[:created_at]).not_to be_nil
+ expect(events.first[:created_at]).not_to be_nil
end
it "has the author's URL" do
- expect(subject.review_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
describe '#staging_events' do
+ let(:stage) { :staging }
let(:merge_request) { MergeRequest.first }
let!(:pipeline) do
create(:ci_pipeline,
@@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do
end
it 'has the name' do
- expect(subject.staging_events.first[:name]).not_to be_nil
+ expect(events.first[:name]).not_to be_nil
end
it 'has the ID' do
- expect(subject.staging_events.first[:id]).not_to be_nil
+ expect(events.first[:id]).not_to be_nil
end
it 'has the URL' do
- expect(subject.staging_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has the branch name' do
- expect(subject.staging_events.first[:branch]).not_to be_nil
+ expect(events.first[:branch]).not_to be_nil
end
it 'has the branch URL' do
- expect(subject.staging_events.first[:branch][:url]).not_to be_nil
+ expect(events.first[:branch][:url]).not_to be_nil
end
it 'has the short SHA' do
- expect(subject.staging_events.first[:short_sha]).not_to be_nil
+ expect(events.first[:short_sha]).not_to be_nil
end
it 'has the commit URL' do
- expect(subject.staging_events.first[:commit_url]).not_to be_nil
+ expect(events.first[:commit_url]).not_to be_nil
end
it 'has the date' do
- expect(subject.staging_events.first[:date]).not_to be_nil
+ expect(events.first[:date]).not_to be_nil
end
it 'has the total time' do
- expect(subject.staging_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it "has the author's URL" do
- expect(subject.staging_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
end
end
describe '#production_events' do
+ let(:stage) { :production }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do
@@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do
end
it 'has the total time' do
- expect(subject.production_events.first[:total_time]).not_to be_empty
+ expect(events.first[:total_time]).not_to be_empty
end
it 'has a title' do
- expect(subject.production_events.first[:title]).to eq(context.title)
+ expect(events.first[:title]).to eq(context.title)
end
it 'has the URL' do
- expect(subject.production_events.first[:url]).not_to be_nil
+ expect(events.first[:url]).not_to be_nil
end
it 'has an iid' do
- expect(subject.production_events.first[:iid]).to eq(context.iid.to_s)
+ expect(events.first[:iid]).to eq(context.iid.to_s)
end
it 'has a created_at timestamp' do
- expect(subject.production_events.first[:created_at]).to end_with('ago')
+ expect(events.first[:created_at]).to end_with('ago')
end
it "has the author's URL" do
- expect(subject.production_events.first[:author][:web_url]).not_to be_nil
+ expect(events.first[:author][:web_url]).not_to be_nil
end
it "has the author's avatar URL" do
- expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil
+ expect(events.first[:author][:avatar_url]).not_to be_nil
end
it "has the author's name" do
- expect(subject.production_events.first[:author][:name]).to eq(context.author.name)
+ expect(events.first[:author][:name]).to eq(context.author.name)
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb
new file mode 100644
index 00000000000..fd9fa2fee49
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::IssueEventFetcher do
+ let(:stage_name) { :issue }
+
+ it_behaves_like 'default query config'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
deleted file mode 100644
index 1c5c308da7d..00000000000
--- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::IssueEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
new file mode 100644
index 00000000000..3127f01989d
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::IssueStage do
+ let(:stage_name) { :issue }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb
index 4a5604115ec..2e5dc5b5547 100644
--- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb
@@ -1,15 +1,13 @@
require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec'
-describe Gitlab::CycleAnalytics::PlanEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
+describe Gitlab::CycleAnalytics::PlanEventFetcher do
+ let(:stage_name) { :plan }
+ it_behaves_like 'default query config' do
context 'no commits' do
it 'does not blow up if there are no commits' do
- allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}])
+ allow(event).to receive(:event_result).and_return([{}])
expect { event.fetch }.not_to raise_error
end
diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
new file mode 100644
index 00000000000..4c715921ad6
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::PlanStage do
+ let(:stage_name) { :plan }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb
new file mode 100644
index 00000000000..74001181305
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ProductionEventFetcher do
+ let(:stage_name) { :production }
+
+ it_behaves_like 'default query config'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
deleted file mode 100644
index ac17e3b4287..00000000000
--- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::ProductionEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb
new file mode 100644
index 00000000000..916684b81eb
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::ProductionStage do
+ let(:stage_name) { :production }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb
new file mode 100644
index 00000000000..4f67c95ed4c
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::ReviewEventFetcher do
+ let(:stage_name) { :review }
+
+ it_behaves_like 'default query config'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
deleted file mode 100644
index 1ff53aa0227..00000000000
--- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::ReviewEvent do
- it_behaves_like 'default query config' do
- it 'has the default order' do
- expect(event.order).to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb
new file mode 100644
index 00000000000..1412c8dfa08
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::ReviewStage do
+ let(:stage_name) { :review }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
index 7019e4c3351..9c5e57342e9 100644
--- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
@@ -1,20 +1,13 @@
require 'spec_helper'
shared_examples 'default query config' do
- let(:event) { described_class.new(project: double, options: {}) }
-
- it 'has the start attributes' do
- expect(event.start_time_attrs).not_to be_nil
- end
+ let(:project) { create(:empty_project) }
+ let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) }
it 'has the stage attribute' do
expect(event.stage).not_to be_nil
end
- it 'has the end attributes' do
- expect(event.end_time_attrs).not_to be_nil
- end
-
it 'has the projection attributes' do
expect(event.projections).not_to be_nil
end
diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
new file mode 100644
index 00000000000..08425acbfc8
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+shared_examples 'base stage' do
+ let(:stage) { described_class.new(project: double, options: {}) }
+
+ before do
+ allow(stage).to receive(:median).and_return(1.12)
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
+ end
+
+ it 'has the median data value' do
+ expect(stage.as_json[:value]).not_to be_nil
+ end
+
+ it 'has the median data stage' do
+ expect(stage.as_json[:title]).not_to be_nil
+ end
+
+ it 'has the median data description' do
+ expect(stage.as_json[:description]).not_to be_nil
+ end
+
+ it 'has the title' do
+ expect(stage.title).to eq(stage_name.to_s.capitalize)
+ end
+
+ it 'has the events' do
+ expect(stage.events).not_to be_nil
+ end
+end
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 725bc68b25f..fb6b6c4a8d2 100644
--- a/spec/models/cycle_analytics/summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -1,23 +1,23 @@
require 'spec_helper'
-describe CycleAnalytics::Summary, models: true do
+describe Gitlab::CycleAnalytics::StageSummary, models: true do
let(:project) { create(:project) }
- let(:from) { Time.now }
+ let(:from) { 1.day.ago }
let(:user) { create(:user, :admin) }
- subject { described_class.new(project, user, from: from) }
+ subject { described_class.new(project, from: Time.now, current_user: user).data }
describe "#new_issues" do
it "finds the number of issues created after the 'from date'" do
Timecop.freeze(5.days.ago) { create(:issue, project: project) }
Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
- expect(subject.new_issues).to eq(1)
+ expect(subject.first[:value]).to eq(1)
end
it "doesn't find issues from other projects" do
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
- expect(subject.new_issues).to eq(0)
+ expect(subject.first[:value]).to eq(0)
end
end
@@ -26,19 +26,19 @@ describe CycleAnalytics::Summary, models: true do
Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
- expect(subject.commits).to eq(1)
+ expect(subject.second[:value]).to eq(1)
end
it "doesn't find commits from other projects" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') }
- expect(subject.commits).to eq(0)
+ expect(subject.second[:value]).to eq(0)
end
it "finds a large (> 100) snumber of commits if present" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
- expect(subject.commits).to eq(100)
+ expect(subject.second[:value]).to eq(100)
end
end
@@ -47,13 +47,13 @@ describe CycleAnalytics::Summary, models: true do
Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
- expect(subject.deploys).to eq(1)
+ expect(subject.third[:value]).to eq(1)
end
it "doesn't find commits from other projects" do
Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) }
- expect(subject.deploys).to eq(0)
+ expect(subject.third[:value]).to eq(0)
end
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb
new file mode 100644
index 00000000000..bbc82496340
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::StagingEventFetcher do
+ let(:stage_name) { :staging }
+
+ it_behaves_like 'default query config' do
+ it 'has a default order' do
+ expect(event.order).not_to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
deleted file mode 100644
index 4862d4765f2..00000000000
--- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::StagingEvent do
- it_behaves_like 'default query config' do
- it 'does not have the default order' do
- expect(event.order).not_to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
new file mode 100644
index 00000000000..8154b3ac701
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::StagingStage do
+ let(:stage_name) { :staging }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb
new file mode 100644
index 00000000000..6639fa54e0e
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_event_spec'
+
+describe Gitlab::CycleAnalytics::TestEventFetcher do
+ let(:stage_name) { :test }
+
+ it_behaves_like 'default query config' do
+ it 'has a default order' do
+ expect(event.order).not_to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
deleted file mode 100644
index e249db69fc6..00000000000
--- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'spec_helper'
-require 'lib/gitlab/cycle_analytics/shared_event_spec'
-
-describe Gitlab::CycleAnalytics::TestEvent do
- it_behaves_like 'default query config' do
- it 'does not have the default order' do
- expect(event.order).not_to eq(event.start_time_attrs)
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
new file mode 100644
index 00000000000..eacde22cd56
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'lib/gitlab/cycle_analytics/shared_stage_spec'
+
+describe Gitlab::CycleAnalytics::TestStage do
+ let(:stage_name) { :test }
+
+ it_behaves_like 'base stage'
+end
diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb
index 19298e261e3..9d806fc524d 100644
--- a/spec/lib/gitlab/email/email_shared_blocks.rb
+++ b/spec/lib/gitlab/email/email_shared_blocks.rb
@@ -18,7 +18,7 @@ shared_context :email_shared_context do
end
end
-shared_examples :email_shared_examples do
+shared_examples :reply_processing_shared_examples do
context "when the user could not be found" do
before do
user.destroy
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 cb3651e3845..08897a4c310 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context
- it_behaves_like :email_shared_examples
+ it_behaves_like :reply_processing_shared_examples
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
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 48660d1dd1b..cebbeff50cf 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
include_context :email_shared_context
- it_behaves_like :email_shared_examples
+ it_behaves_like :reply_processing_shared_examples
before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
diff --git a/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
new file mode 100644
index 00000000000..a444257754b
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/unsubscribe_handler_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+describe Gitlab::Email::Handler::UnsubscribeHandler, lib: true do
+ include_context :email_shared_context
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: 'reply+%{key}@appmail.adventuretime.ooo')
+ stub_config_setting(host: 'localhost')
+ end
+
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}+unsubscribe") }
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:noteable) { create(:issue, project: project) }
+
+ let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
+
+ context 'when notification concerns a commit' do
+ let(:commit) { create(:commit, project: project) }
+ let!(:sent_notification) { SentNotification.record(commit, user.id, mail_key) }
+
+ it 'handler does not raise an error' do
+ expect { receiver.execute }.not_to raise_error
+ end
+ end
+
+ context 'user is unsubscribed' do
+ it 'leaves user unsubscribed' do
+ expect { receiver.execute }.not_to change { noteable.subscribed?(user) }.from(false)
+ end
+ end
+
+ context 'user is subscribed' do
+ before do
+ noteable.subscribe(user)
+ end
+
+ it 'unsubscribes user from notable' do
+ expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false)
+ end
+ end
+
+ context 'when the noteable could not be found' do
+ before do
+ noteable.destroy
+ end
+
+ it 'raises a NoteableNotFoundError' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError)
+ end
+ end
+
+ context 'when no sent notification for the mail key could be found' do
+ let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') }
+
+ it 'raises a SentNotificationNotFoundError' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ceed9c942c1..7fb6829f582 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -15,6 +15,7 @@ issues:
- events
- merge_requests_closing_issues
- metrics
+- timelogs
events:
- author
- project
@@ -77,6 +78,7 @@ merge_requests:
- events
- merge_requests_closing_issues
- metrics
+- timelogs
merge_request_diff:
- merge_request
pipelines:
@@ -198,3 +200,6 @@ award_emoji:
- user
priorities:
- label
+timelogs:
+- trackable
+- user
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 1cb02f8e318..b069696b5c7 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::MembersMapper, services: true do
describe 'map members' do
- let(:user) { create(:user, authorized_projects_populated: true) }
+ let(:user) { create(:admin, authorized_projects_populated: true) }
let(:project) { create(:project, :public, name: 'searchable_project') }
let(:user2) { create(:user, authorized_projects_populated: true) }
let(:exported_user_id) { 99 }
@@ -24,7 +24,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
{
"id" => exported_user_id,
"email" => user2.email,
- "username" => user2.username
+ "username" => 'test'
}
},
{
@@ -48,6 +48,10 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
exported_members: exported_members, user: user, project: project)
end
+ it 'includes the exported user ID in the map' do
+ expect(members_mapper.map.keys).to include(exported_user_id)
+ end
+
it 'maps a project member' do
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
end
@@ -56,12 +60,6 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(members_mapper.map[-1]).to eq(user.id)
end
- it 'updates missing author IDs on missing project member' do
- members_mapper.map[-1]
-
- expect(members_mapper.missing_author_ids.first).to eq(-1)
- end
-
it 'has invited members with no user' do
members_mapper.map
@@ -74,5 +72,25 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(user.authorized_project?(project)).to be true
expect(user2.authorized_project?(project)).to be true
end
+
+ context 'user is not an admin' do
+ let(:user) { create(:user, authorized_projects_populated: true) }
+
+ it 'does not map a project member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user.id)
+ end
+
+ it 'defaults to importer project member if it does not exist' do
+ expect(members_mapper.map[-1]).to eq(user.id)
+ end
+ end
+
+ context 'chooses the one with an email first' do
+ let(:user3) { create(:user, username: 'test') }
+
+ it 'maps the project member that has a matching email first' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index 3aa492a8ab1..db0084d6823 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::RelationFactory, lib: true do
let(:project) { create(:empty_project) }
let(:members_mapper) { double('members_mapper').as_null_object }
- let(:user) { create(:user) }
+ let(:user) { create(:admin) }
let(:created_object) do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
@@ -122,4 +122,60 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
expect(created_object.values).not_to include(99)
end
end
+
+ context 'Notes user references' do
+ let(:relation_sym) { :notes }
+ let(:new_user) { create(:user) }
+ let(:exported_member) do
+ {
+ "id" => 111,
+ "access_level" => 30,
+ "source_id" => 1,
+ "source_type" => "Project",
+ "user_id" => 3,
+ "notification_level" => 3,
+ "created_at" => "2016-11-18T09:29:42.634Z",
+ "updated_at" => "2016-11-18T09:29:42.634Z",
+ "user" => {
+ "id" => 999,
+ "email" => new_user.email,
+ "username" => new_user.username
+ }
+ }
+ end
+
+ let(:relation_hash) do
+ {
+ "id" => 4947,
+ "note" => "merged",
+ "noteable_type" => "MergeRequest",
+ "author_id" => 999,
+ "created_at" => "2016-11-18T09:29:42.634Z",
+ "updated_at" => "2016-11-18T09:29:42.634Z",
+ "project_id" => 1,
+ "attachment" => {
+ "url" => nil
+ },
+ "noteable_id" => 377,
+ "system" => true,
+ "author" => {
+ "name" => "Administrator"
+ },
+ "events" => [
+
+ ]
+ }
+ end
+
+ let(:members_mapper) do
+ Gitlab::ImportExport::MembersMapper.new(
+ exported_members: [exported_member],
+ user: user,
+ project: project)
+ end
+
+ it 'maps the right author to the imported note' do
+ expect(created_object.author).to eq(new_user)
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index d88a141b458..493bc2db21a 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -20,6 +20,7 @@ Issue:
- lock_version
- milestone_id
- weight
+- time_estimate
Event:
- id
- target_type
@@ -150,6 +151,7 @@ MergeRequest:
- milestone_id
- approvals_before_merge
- rebase_commit_sha
+- time_estimate
MergeRequestDiff:
- id
- state
@@ -344,3 +346,11 @@ LabelPriority:
- priority
- created_at
- updated_at
+Timelog:
+- id
+- time_spent
+- trackable_id
+- trackable_type
+- user_id
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 1dcf2c0668b..7e951e3fcdd 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do
end
end
+ describe 'self.supports_wildcard?' do
+ context 'address contains the wildard placeholder' do
+ before do
+ stub_incoming_email_setting(address: 'replies+%{key}@example.com')
+ end
+
+ it 'confirms that wildcard is supported' do
+ expect(described_class.supports_wildcard?).to be_truthy
+ end
+ end
+
+ context "address doesn't contain the wildcard placeholder" do
+ before do
+ stub_incoming_email_setting(address: 'replies@example.com')
+ end
+
+ it 'returns that wildcard is not supported' do
+ expect(described_class.supports_wildcard?).to be_falsey
+ end
+ end
+
+ context 'address is not set' do
+ before do
+ stub_incoming_email_setting(address: nil)
+ end
+
+ it 'returns that wildard is not supported' do
+ expect(described_class.supports_wildcard?).to be_falsey
+ end
+ end
+ end
+
+ context 'self.unsubscribe_address' do
+ before do
+ stub_incoming_email_setting(address: 'replies+%{key}@example.com')
+ end
+
+ it 'returns the address with interpolated reply key and unsubscribe suffix' do
+ expect(described_class.unsubscribe_address('key')).to eq('replies+key+unsubscribe@example.com')
+ end
+ end
+
context "self.reply_address" do
before do
stub_incoming_email_setting(address: "replies+%{key}@example.com")
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 14ee386dba6..d94eb52f838 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -178,4 +178,119 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.objects('notes')).not_to include note
end
end
+
+ # Examples for commit access level test
+ #
+ # params:
+ # * search_phrase
+ # * commit
+ #
+ shared_examples 'access restricted commits' do
+ context 'when project is internal' do
+ let(:project) { create(:project, :internal) }
+
+ it 'does not search if user is not authenticated' do
+ commits = described_class.new(nil, project, search_phrase).objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ it 'searches if user is authenticated' do
+ commits = described_class.new(user, project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+ end
+
+ context 'when project is private' do
+ let!(:creator) { create(:user, username: 'private-project-author') }
+ let!(:private_project) { create(:project, :private, creator: creator, namespace: creator.namespace) }
+ let(:team_master) do
+ user = create(:user, username: 'private-project-master')
+ private_project.team << [user, :master]
+ user
+ end
+ let(:team_reporter) do
+ user = create(:user, username: 'private-project-reporter')
+ private_project.team << [user, :reporter]
+ user
+ end
+
+ it 'does not show commit to stranger' do
+ commits = described_class.new(nil, private_project, search_phrase).objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ context 'team access' do
+ it 'shows commit to creator' do
+ commits = described_class.new(creator, private_project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+
+ it 'shows commit to master' do
+ commits = described_class.new(team_master, private_project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+
+ it 'shows commit to reporter' do
+ commits = described_class.new(team_reporter, private_project, search_phrase).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+ end
+ end
+ end
+
+ describe 'commit search' do
+ context 'by commit message' do
+ let(:project) { create(:project, :public) }
+ let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') }
+ let(:message) { 'Sorry, I did a mistake' }
+
+ it 'finds commit by message' do
+ commits = described_class.new(user, project, message).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+
+ it 'handles when no commit match' do
+ commits = described_class.new(user, project, 'not really an existing description').objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ it_behaves_like 'access restricted commits' do
+ let(:search_phrase) { message }
+ let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') }
+ end
+ end
+
+ context 'by commit hash' do
+ let(:project) { create(:project, :public) }
+ let(:commit) { project.repository.commit('0b4bc9a') }
+ commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+
+ commit_hashes.each do |type, commit_hash|
+ it "shows commit by #{type} hash id" do
+ commits = described_class.new(user, project, commit_hash).objects('commits')
+
+ expect(commits).to contain_exactly commit
+ end
+ end
+
+ it 'handles not existing commit hash correctly' do
+ commits = described_class.new(user, project, 'deadbeef').objects('commits')
+
+ expect(commits).to be_empty
+ end
+
+ it_behaves_like 'access restricted commits' do
+ let(:search_phrase) { '0b4bc9a49' }
+ let(:commit) { project.repository.commit('0b4bc9a') }
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index d3c3b800b94..369e55f61f1 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do
end
describe 'push to protected branch' do
- let(:branch) { create :protected_branch, project: project }
+ let(:branch) { create :protected_branch, project: project, name: "test" }
+ let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
it 'returns true if user is a master' do
project.team << [user, :master]
@@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
+
+ it 'returns true if branch does not exist and user has permission to merge' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy
+ end
end
describe 'push to protected branch if allowed for developers' do
diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb
new file mode 100644
index 00000000000..f2c152cdcd4
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/base_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Base do
+ let(:project) { double(:project) }
+ let(:presenter_class) do
+ Struct.new(:subject).include(described_class)
+ end
+
+ describe '.presenter?' do
+ it 'returns true' do
+ presenter = presenter_class.new(project)
+
+ expect(presenter.class).to be_presenter
+ end
+ end
+
+ describe '.presents' do
+ it 'exposes #subject with the given keyword' do
+ presenter_class.presents(:foo)
+ presenter = presenter_class.new(project)
+
+ expect(presenter.foo).to eq(project)
+ end
+ end
+
+ describe '#can?' do
+ context 'user is not allowed' do
+ it 'returns false' do
+ presenter = presenter_class.new(build_stubbed(:empty_project))
+
+ expect(presenter.can?(nil, :read_project)).to be_falsy
+ end
+ end
+
+ context 'user is allowed' do
+ it 'returns true' do
+ presenter = presenter_class.new(build_stubbed(:empty_project, :public))
+
+ expect(presenter.can?(nil, :read_project)).to be_truthy
+ end
+ end
+
+ context 'subject is overriden' do
+ it 'returns true' do
+ presenter = presenter_class.new(build_stubbed(:empty_project, :public))
+
+ expect(presenter.can?(nil, :read_project, build_stubbed(:empty_project))).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/view/presenter/delegated_spec.rb b/spec/lib/gitlab/view/presenter/delegated_spec.rb
new file mode 100644
index 00000000000..888ab80cad5
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/delegated_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Delegated do
+ let(:project) { double(:project, bar: 'baz') }
+ let(:presenter_class) do
+ Class.new(described_class)
+ end
+
+ it 'includes Gitlab::View::Presenter::Base' do
+ expect(described_class).to include(Gitlab::View::Presenter::Base)
+ end
+
+ describe '#initialize' do
+ it 'takes arbitrary key/values and exposes them' do
+ presenter = presenter_class.new(project, user: 'user', foo: 'bar')
+
+ expect(presenter.user).to eq('user')
+ expect(presenter.foo).to eq('bar')
+ end
+ end
+
+ describe 'delegation' do
+ it 'forwards missing methods to subject' do
+ presenter = presenter_class.new(project)
+
+ expect(presenter.bar).to eq('baz')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb
new file mode 100644
index 00000000000..55c5ecbf92f
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/factory_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Factory do
+ let(:build) { Ci::Build.new }
+
+ describe '#initialize' do
+ context 'without optional parameters' do
+ it 'takes a subject and optional params' do
+ presenter = described_class.new(build)
+
+ expect { presenter }.not_to raise_error
+ end
+ end
+
+ context 'with optional parameters' do
+ it 'takes a subject and optional params' do
+ presenter = described_class.new(build, user: 'user')
+
+ expect { presenter }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#fabricate!' do
+ it 'exposes given params' do
+ presenter = described_class.new(build, user: 'user', foo: 'bar').fabricate!
+
+ expect(presenter.user).to eq('user')
+ expect(presenter.foo).to eq('bar')
+ end
+
+ it 'detects the presenter based on the given subject' do
+ presenter = described_class.new(build).fabricate!
+
+ expect(presenter).to be_a(Ci::BuildPresenter)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/view/presenter/simple_spec.rb b/spec/lib/gitlab/view/presenter/simple_spec.rb
new file mode 100644
index 00000000000..b489bdf1981
--- /dev/null
+++ b/spec/lib/gitlab/view/presenter/simple_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::View::Presenter::Simple do
+ let(:project) { double(:project) }
+ let(:presenter_class) do
+ Class.new(described_class)
+ end
+
+ it 'includes Gitlab::View::Presenter::Base' do
+ expect(described_class).to include(Gitlab::View::Presenter::Base)
+ end
+
+ describe '#initialize' do
+ it 'takes arbitrary key/values and exposes them' do
+ presenter = presenter_class.new(project, user: 'user', foo: 'bar')
+
+ expect(presenter.user).to eq('user')
+ expect(presenter.foo).to eq('bar')
+ end
+ end
+
+ describe 'delegation' do
+ it 'does not forward missing methods to subject' do
+ presenter = presenter_class.new(project)
+
+ expect { presenter.foo }.to raise_error(NoMethodError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 61da91dcbd3..4b1cd466677 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -174,4 +174,27 @@ describe Gitlab::Workhorse, lib: true do
described_class.verify_api_request!(headers)
end
end
+
+ describe '.git_http_ok' do
+ let(:user) { create(:user) }
+
+ subject { described_class.git_http_ok(repository, user) }
+
+ it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repository.path_to_repo }) }
+
+ context 'when Gitaly socket path is present' do
+ let(:gitaly_socket_path) { '/tmp/gitaly.sock' }
+
+ before do
+ allow(Gitlab.config.gitaly).to receive(:socket_path).and_return(gitaly_socket_path)
+ end
+
+ it 'includes Gitaly params in the returned value' do
+ expect(subject).to include({
+ GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
+ GitalySocketPath: gitaly_socket_path,
+ })
+ end
+ end
+ end
end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 1bdf005c823..4d57efd3c53 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -171,6 +171,33 @@ describe Ability, lib: true do
end
end
+ describe '.users_that_can_read_personal_snippet' do
+ def users_for_snippet(snippet)
+ described_class.users_that_can_read_personal_snippet(users, snippet)
+ end
+
+ let(:users) { create_list(:user, 3) }
+ let(:author) { users[0] }
+
+ it 'private snippet is readable only by its author' do
+ snippet = create(:personal_snippet, :private, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array([author])
+ end
+
+ it 'internal snippet is readable by all registered users' do
+ snippet = create(:personal_snippet, :public, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array(users)
+ end
+
+ it 'public snippet is readable by all users' do
+ snippet = create(:personal_snippet, :public, author: author)
+
+ expect(users_for_snippet(snippet)).to match_array(users)
+ end
+ end
+
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 3309a7fff9f..f031876e812 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1401,4 +1401,14 @@ describe Ci::Build, :models do
it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
end
end
+
+ describe 'State transition: any => [:pending]' do
+ let(:build) { create(:ci_build, :created) }
+
+ it 'queues BuildQueueWorker' do
+ expect(BuildQueueWorker).to receive(:perform_async).with(build.id)
+
+ build.enqueue
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d1aee27057a..2bdd611aeed 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -122,55 +122,80 @@ describe Ci::Pipeline, models: true do
end
end
- describe '#stages' do
+ describe 'pipeline stages' do
before do
- create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success')
- create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed')
- create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running')
- create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success')
- end
-
- subject { pipeline.stages }
-
- context 'stages list' do
- it 'returns ordered list of stages' do
- expect(subject.map(&:name)).to eq(%w[build test deploy])
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'linux',
+ stage_idx: 0,
+ status: 'success')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'failed')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'staging',
+ stage_idx: 2,
+ status: 'running')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'test',
+ name: 'rspec',
+ stage_idx: 1,
+ status: 'success')
+ end
+
+ describe '#stages' do
+ subject { pipeline.stages }
+
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
end
- end
- it 'returns a valid number of stages' do
- expect(pipeline.stages_count).to eq(3)
- end
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map { |stage| [stage.name, stage.status] }
+ end
- it 'returns a valid names of stages' do
- expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
- end
+ it 'returns list of stages with correct statuses' do
+ expect(statuses).to eq([['build', 'failed'],
+ ['test', 'success'],
+ ['deploy', 'running']])
+ end
- context 'stages with statuses' do
- let(:statuses) do
- subject.map do |stage|
- [stage.name, stage.status]
+ context 'when commit status is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'success')
+ end
+
+ it 'ignores the previous state' do
+ expect(statuses).to eq([['build', 'success'],
+ ['test', 'success'],
+ ['deploy', 'running']])
+ end
end
end
+ end
- it 'returns list of stages with statuses' do
- expect(statuses).to eq([['build', 'failed'],
- ['test', 'success'],
- ['deploy', 'running']
- ])
+ describe '#stages_count' do
+ it 'returns a valid number of stages' do
+ expect(pipeline.stages_count).to eq(3)
end
+ end
- context 'when build is retried' do
- before do
- create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success')
- end
-
- it 'ignores the previous state' do
- expect(statuses).to eq([['build', 'success'],
- ['test', 'success'],
- ['deploy', 'running']
- ])
- end
+ describe '#stages_name' do
+ it 'returns a valid names of stages' do
+ expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index ef65eb99328..2b856ca7af7 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -263,6 +263,62 @@ describe Ci::Runner, models: true do
end
end
+ describe '#tick_runner_queue' do
+ let(:runner) { create(:ci_runner) }
+
+ it 'returns a new last_update value' do
+ expect(runner.tick_runner_queue).not_to be_empty
+ end
+ end
+
+ describe '#ensure_runner_queue_value' do
+ let(:runner) { create(:ci_runner) }
+
+ it 'sets a new last_update value when it is called the first time' do
+ last_update = runner.ensure_runner_queue_value
+
+ expect_value_in_redis.to eq(last_update)
+ end
+
+ it 'does not change if it is not expired and called again' do
+ last_update = runner.ensure_runner_queue_value
+
+ expect(runner.ensure_runner_queue_value).to eq(last_update)
+ expect_value_in_redis.to eq(last_update)
+ end
+
+ context 'updates runner queue after changing editable value' do
+ let!(:last_update) { runner.ensure_runner_queue_value }
+
+ before do
+ runner.update(description: 'new runner')
+ end
+
+ it 'sets a new last_update value' do
+ expect_value_in_redis.not_to eq(last_update)
+ end
+ end
+
+ context 'does not update runner value after save' do
+ let!(:last_update) { runner.ensure_runner_queue_value }
+
+ before do
+ runner.touch
+ end
+
+ it 'has an old last_update value' do
+ expect_value_in_redis.to eq(last_update)
+ end
+ end
+
+ def expect_value_in_redis
+ Gitlab::Redis.with do |redis|
+ runner_queue_key = runner.send(:runner_queue_key)
+ expect(redis.get(runner_queue_key))
+ end
+ end
+ end
+
describe '.assignable_for' do
let(:runner) { create(:ci_runner) }
let(:project) { create(:project) }
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 742bedb37e4..c4a9743a4e2 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -142,6 +142,78 @@ describe Ci::Stage, models: true do
end
end
+ describe '#success?' do
+ context 'when stage is successful' do
+ before do
+ create_job(:ci_build, status: :success)
+ create_job(:generic_commit_status, status: :success)
+ end
+
+ it 'is successful' do
+ expect(stage).to be_success
+ end
+ end
+
+ context 'when stage is not successful' do
+ before do
+ create_job(:ci_build, status: :failed)
+ create_job(:generic_commit_status, status: :success)
+ end
+
+ it 'is not successful' do
+ expect(stage).not_to be_success
+ end
+ end
+ end
+
+ describe '#has_warnings?' do
+ context 'when stage has warnings' do
+ context 'when using memoized warnings flag' do
+ context 'when there are warnings' do
+ let(:stage) { build(:ci_stage, warnings: true) }
+
+ it 'has memoized warnings' do
+ expect(stage).not_to receive(:statuses)
+ expect(stage).to have_warnings
+ end
+ end
+
+ context 'when there are no warnings' do
+ let(:stage) { build(:ci_stage, warnings: false) }
+
+ it 'has memoized warnings' do
+ expect(stage).not_to receive(:statuses)
+ expect(stage).not_to have_warnings
+ end
+ end
+ end
+
+ context 'when calculating warnings from statuses' do
+ before do
+ create(:ci_build, :failed, :allowed_to_fail,
+ stage: stage_name, pipeline: pipeline)
+ end
+
+ it 'has warnings calculated from statuses' do
+ expect(stage).to receive(:statuses).and_call_original
+ expect(stage).to have_warnings
+ end
+ end
+ end
+
+ context 'when stage does not have warnings' do
+ before do
+ create(:ci_build, :success, stage: stage_name,
+ pipeline: pipeline)
+ end
+
+ it 'does not have warnings calculated from statuses' do
+ expect(stage).to receive(:statuses).and_call_original
+ expect(stage).not_to have_warnings
+ end
+ end
+ end
+
def create_job(type, status: 'success', stage: stage_name)
create(type, pipeline: pipeline, stage: stage, status: status)
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 74b50d2908d..b2202f0fd44 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -323,4 +323,50 @@ eos
expect(new_commit.message).to eq(commit.message)
end
end
+
+ describe '#work_in_progress?' do
+ ['squash! ', 'fixup! ', 'wip: ', 'WIP: ', '[WIP] '].each do |wip_prefix|
+ it "detects the '#{wip_prefix}' prefix" do
+ commit.message = "#{wip_prefix}#{commit.message}"
+
+ expect(commit).to be_work_in_progress
+ end
+ end
+
+ it "detects WIP for a commit just saying 'wip'" do
+ commit.message = "wip"
+
+ expect(commit).to be_work_in_progress
+ end
+
+ it "doesn't detect WIP for a commit that begins with 'FIXUP! '" do
+ commit.message = "FIXUP! #{commit.message}"
+
+ expect(commit).not_to be_work_in_progress
+ end
+
+ it "doesn't detect WIP for words starting with WIP" do
+ commit.message = "Wipout #{commit.message}"
+
+ expect(commit).not_to be_work_in_progress
+ end
+ end
+
+ describe '.valid_hash?' do
+ it 'checks hash contents' do
+ expect(described_class.valid_hash?('abcdef01239ABCDEF')).to be true
+ expect(described_class.valid_hash?("abcdef01239ABCD\nEF")).to be false
+ expect(described_class.valid_hash?(' abcdef01239ABCDEF ')).to be false
+ expect(described_class.valid_hash?('Gabcdef01239ABCDEF')).to be false
+ expect(described_class.valid_hash?('gabcdef01239ABCDEF')).to be false
+ expect(described_class.valid_hash?('-abcdef01239ABCDEF')).to be false
+ end
+
+ it 'checks hash length' do
+ expect(described_class.valid_hash?('a' * 6)).to be false
+ expect(described_class.valid_hash?('a' * 7)).to be true
+ expect(described_class.valid_hash?('a' * 40)).to be true
+ expect(described_class.valid_hash?('a' * 41)).to be false
+ end
+ end
end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 4d0f51fe82a..dbfe3cd2d36 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -219,4 +219,10 @@ describe HasStatus do
end
end
end
+
+ describe '::DEFAULT_STATUS' do
+ it 'is a status created' do
+ expect(described_class::DEFAULT_STATUS).to eq 'created'
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 1078c959419..d7d31892e12 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -408,4 +408,42 @@ describe Issue, "Issuable" do
expect(issue.assignee_or_author?(user)).to eq(false)
end
end
+
+ describe '#spend_time' do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue) }
+
+ def spend_time(seconds)
+ issue.spend_time(duration: seconds, user: user)
+ issue.save!
+ end
+
+ context 'adding time' do
+ it 'should update the total time spent' do
+ spend_time(1800)
+
+ expect(issue.total_time_spent).to eq(1800)
+ end
+ end
+
+ context 'substracting time' do
+ before do
+ spend_time(1800)
+ end
+
+ it 'should update the total time spent' do
+ spend_time(-900)
+
+ expect(issue.total_time_spent).to eq(900)
+ end
+
+ 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)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 132858950d5..b73028f0bc0 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -30,12 +30,20 @@ describe Issue, "Mentionable" do
describe '#mentioned_users' do
let!(:user) { create(:user, username: 'stranger') }
let!(:user2) { create(:user, username: 'john') }
- let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
+ let!(:user3) { create(:user, username: 'jim') }
+ let(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
subject { issue.mentioned_users }
- it { is_expected.to include(user) }
- it { is_expected.not_to include(user2) }
+ it { expect(subject).to contain_exactly(user) }
+
+ context 'when a note on personal snippet' do
+ let!(:note) { create(:note_on_personal_snippet, note: "#{user.to_reference} mentioned #{user3.to_reference}") }
+
+ subject { note.mentioned_users }
+
+ it { expect(subject).to contain_exactly(user, user3) }
+ end
end
describe '#referenced_mentionables' do
@@ -138,6 +146,16 @@ describe Issue, "Mentionable" do
issue.update_attributes(description: issues[1].to_reference)
issue.create_new_cross_references!
end
+
+ it 'notifies new references from project snippet note' do
+ snippet = create(:snippet, project: project)
+ note = create(:note, note: issues[0].to_reference, noteable: snippet, project: project, author: author)
+
+ expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
+
+ note.update_attributes(note: issues[1].to_reference)
+ note.create_new_cross_references!
+ end
end
def create_issue(description:)
diff --git a/spec/models/concerns/presentable_spec.rb b/spec/models/concerns/presentable_spec.rb
new file mode 100644
index 00000000000..941647a79fb
--- /dev/null
+++ b/spec/models/concerns/presentable_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Presentable do
+ let(:build) { Ci::Build.new }
+
+ describe '#present' do
+ it 'returns a presenter' do
+ expect(build.present).to be_a(Ci::BuildPresenter)
+ end
+
+ it 'takes optional attributes' do
+ expect(build.present(foo: 'bar').foo).to eq('bar')
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 7771785ead3..70f985afefb 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
context 'with deployment' do
generate_cycle_analytics_spec(
@@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do
-> (context, data) do
context.create_commit_referencing_issue(data[:issue])
end]],
- end_time_conditions: [["merge request that closes issue is created",
- -> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
- end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.deploy_master
@@ -37,7 +37,7 @@ describe 'CycleAnalytics#code', feature: true do
deploy_master
end
- expect(subject.code).to be_nil
+ expect(subject[:code].median).to be_nil
end
end
end
@@ -50,10 +50,10 @@ describe 'CycleAnalytics#code', feature: true do
-> (context, data) do
context.create_commit_referencing_issue(data[:issue])
end]],
- end_time_conditions: [["merge request that closes issue is created",
- -> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
- end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
end)
@@ -69,7 +69,7 @@ describe 'CycleAnalytics#code', feature: true do
merge_merge_requests_closing_issue(issue)
end
- expect(subject.code).to be_nil
+ expect(subject[:code].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 5ed3d37f2fb..e4b6a8f4518 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :issue,
@@ -42,7 +42,7 @@ describe 'CycleAnalytics#issue', models: true do
merge_merge_requests_closing_issue(issue)
end
- expect(subject.issue).to be_nil
+ expect(subject[:issue].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index baf3e3241a1..dc5b04852d6 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :plan,
@@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do
create_merge_request_closing_issue(issue, source_branch: branch_name)
merge_merge_requests_closing_issue(issue)
- expect(subject.issue).to be_nil
+ expect(subject[:issue].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 21b9c6e7150..5e99188f318 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :production,
@@ -35,7 +35,7 @@ describe 'CycleAnalytics#production', feature: true do
deploy_master
end
- expect(subject.production).to be_nil
+ expect(subject[:production].median).to be_nil
end
end
@@ -48,7 +48,7 @@ describe 'CycleAnalytics#production', feature: true do
deploy_master(environment: 'staging')
end
- expect(subject.production).to be_nil
+ expect(subject[:production].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 158621d59a4..45baa5f7006 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :review,
@@ -27,7 +27,7 @@ describe 'CycleAnalytics#review', feature: true do
MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
end
- expect(subject.review).to be_nil
+ expect(subject[:review].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index dad653964b7..77625aad580 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :staging,
@@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging', feature: true do
deploy_master
end
- expect(subject.staging).to be_nil
+ expect(subject[:staging].median).to be_nil
end
end
@@ -58,7 +58,7 @@ describe 'CycleAnalytics#staging', feature: true do
deploy_master(environment: 'staging')
end
- expect(subject.staging).to be_nil
+ expect(subject[:staging].median).to be_nil
end
end
end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 2313724e8f3..27a117d2d76 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do
let(:project) { create(:project) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, user, from: from_date) }
+ subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec(
phase: :test,
@@ -35,7 +35,7 @@ describe 'CycleAnalytics#test', feature: true do
merge_merge_requests_closing_issue(issue)
end
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
@@ -48,7 +48,7 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.succeed!
end
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
@@ -65,7 +65,7 @@ describe 'CycleAnalytics#test', feature: true do
merge_merge_requests_closing_issue(issue)
end
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
@@ -82,7 +82,7 @@ describe 'CycleAnalytics#test', feature: true do
merge_merge_requests_closing_issue(issue)
end
- expect(subject.test).to be_nil
+ expect(subject[:test].median).to be_nil
end
end
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 6004bfdb7b7..f4c3e6d503f 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,10 +1,20 @@
require 'spec_helper'
describe GenericCommitStatus, models: true do
- let(:pipeline) { create(:ci_pipeline) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:external_url) { 'http://example.gitlab.com/status' }
let(:generic_commit_status) do
- create(:generic_commit_status, pipeline: pipeline)
+ create(:generic_commit_status, pipeline: pipeline,
+ target_url: external_url)
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_length_of(:target_url).is_at_most(255) }
+ it { is_expected.to allow_value(nil).for(:target_url) }
+ it { is_expected.to allow_value('http://gitlab.com/s').for(:target_url) }
+ it { is_expected.not_to allow_value('javascript:alert(1)').for(:target_url) }
end
describe '#context' do
@@ -22,10 +32,25 @@ describe GenericCommitStatus, models: true do
describe '#detailed_status' do
let(:user) { create(:user) }
+ let(:status) { generic_commit_status.detailed_status(user) }
it 'returns detailed status object' do
- expect(generic_commit_status.detailed_status(user))
- .to be_a Gitlab::Ci::Status::Success
+ expect(status).to be_a Gitlab::Ci::Status::Success
+ end
+
+ context 'when user has ability to see datails' do
+ before { project.team << [user, :developer] }
+
+ it 'details path points to an external URL' do
+ expect(status).to have_details
+ expect(status.details_path).to eq external_url
+ end
+ end
+
+ context 'when user should not see details' do
+ it 'does not have details' do
+ expect(status).not_to have_details
+ end
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 5eaddd822be..7c40cfd8253 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -30,11 +30,30 @@ describe Key, models: true do
end
describe "#update_last_used_at" do
- it "enqueues a UseKeyWorker job" do
- key = create(:key)
+ let(:key) { create(:key) }
+
+ context 'when key was not updated during the last day' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return('000000')
+ end
+
+ it 'enqueues a UseKeyWorker job' do
+ expect(UseKeyWorker).to receive(:perform_async).with(key.id)
+ key.update_last_used_at
+ end
+ end
+
+ context 'when key was updated during the last day' do
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return(false)
+ end
- expect(UseKeyWorker).to receive(:perform_async).with(key.id)
- key.update_last_used_at
+ it 'does not enqueue a UseKeyWorker job' do
+ expect(UseKeyWorker).not_to receive(:perform_async)
+ key.update_last_used_at
+ end
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 861426acbc3..32ed1e96749 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -65,7 +65,7 @@ describe MergeRequest, models: true do
end
describe '#target_branch_sha' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
subject { create(:merge_request, source_project: project, target_project: project) }
@@ -150,7 +150,7 @@ describe MergeRequest, models: true do
end
it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
+ another_project = build(:empty_project, name: 'another-project', namespace: project.namespace)
expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
end
@@ -245,8 +245,8 @@ describe MergeRequest, models: true do
describe '#for_fork?' do
it 'returns true if the merge request is for a fork' do
- subject.source_project = create(:project, namespace: create(:group))
- subject.target_project = create(:project, namespace: create(:group))
+ subject.source_project = build_stubbed(:empty_project, namespace: create(:group))
+ subject.target_project = build_stubbed(:empty_project, namespace: create(:group))
expect(subject.for_fork?).to be_truthy
end
@@ -501,8 +501,8 @@ describe MergeRequest, models: true do
end
describe '#diverged_commits_count' do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, forked_from_project: project) }
context 'when the target branch does not exist anymore' do
subject { create(:merge_request, source_project: project, target_project: project) }
@@ -727,7 +727,7 @@ describe MergeRequest, models: true do
end
describe '#participants' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:mr) do
create(:merge_request, source_project: project, target_project: project)
@@ -768,15 +768,17 @@ describe MergeRequest, models: true do
end
describe '#check_if_can_be_merged' do
- let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) }
+ let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
context 'when it is not broken and has no conflicts' do
- it 'is marked as mergeable' do
+ before do
allow(subject).to receive(:broken?) { false }
allow(project.repository).to receive(:can_be_merged?).and_return(true)
+ 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
@@ -787,6 +789,12 @@ describe MergeRequest, models: true do
it 'becomes unmergeable' do
expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
end
+
+ it 'creates Todo on unmergeability' do
+ expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject)
+
+ subject.check_if_can_be_merged
+ end
end
context 'when it has conflicts' do
@@ -802,7 +810,7 @@ describe MergeRequest, models: true do
end
describe '#mergeable?' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
subject { create(:merge_request, source_project: project) }
@@ -822,7 +830,7 @@ describe MergeRequest, models: true do
end
describe '#mergeable_state?' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
subject { create(:merge_request, source_project: project) }
@@ -949,7 +957,7 @@ describe MergeRequest, models: true do
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
context 'when project.only_allow_merge_if_all_discussions_are_resolved == true' do
- let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
context 'with all discussions resolved' do
before do
@@ -983,7 +991,7 @@ describe MergeRequest, models: true do
end
context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do
- let(:project) { create(:project, only_allow_merge_if_all_discussions_are_resolved: false) }
+ let(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: false) }
context 'with unresolved discussions' do
before do
@@ -998,7 +1006,7 @@ describe MergeRequest, models: true do
end
describe "#environments" do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
context 'with multiple environments' do
@@ -1016,7 +1024,7 @@ describe MergeRequest, models: true do
context 'with environments on source project' do
let(:source_project) do
- create(:project) do |fork_project|
+ create(:project, :repository) do |fork_project|
fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
end
end
@@ -1393,8 +1401,8 @@ describe MergeRequest, models: true do
end
describe "#source_project_missing?" do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
let(:user) { create(:user) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
@@ -1431,8 +1439,8 @@ describe MergeRequest, models: true do
end
describe "#closed_without_fork?" do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
let(:user) { create(:user) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
@@ -1477,9 +1485,9 @@ describe MergeRequest, models: true do
end
context 'forked project' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user.namespace) }
let!(:merge_request) do
create(:closed_merge_request,
@@ -1523,7 +1531,7 @@ describe MergeRequest, models: true do
status: status)
end
- let(:project) { create(:project, :public, only_allow_merge_if_build_succeeds: true) }
+ let(:project) { create(:project, :public, :repository, only_allow_merge_if_build_succeeds: true) }
let(:developer) { create(:user) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 600538ff5f4..f8e03fa114a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -117,6 +117,7 @@ describe Namespace, models: true do
new_path = @namespace.path + "_new"
allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return(new_path)
+ expect(@namespace).to receive(:remove_exports!)
expect(@namespace.move_dir).to be_truthy
end
@@ -139,11 +140,17 @@ describe Namespace, models: true do
let!(:project) { create(:project, namespace: namespace) }
let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
- before { namespace.destroy }
-
it "removes its dirs when deleted" do
+ namespace.destroy
+
expect(File.exist?(path)).to be(false)
end
+
+ it 'removes the exports folder' do
+ expect(namespace).to receive(:remove_exports!)
+
+ namespace.destroy
+ end
end
describe '.find_by_path_or_name' do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 310fecd8a5c..1b8ae356f1f 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -52,6 +52,19 @@ describe Note, models: true do
subject { create(:note) }
it { is_expected.to be_valid }
end
+
+ context 'when project is missing for a project related note' do
+ subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when noteable is a personal snippet' do
+ subject { build(:note_on_personal_snippet) }
+
+ it 'is valid without project' do
+ is_expected.to be_valid
+ end
+ end
end
describe "Commit notes" do
@@ -139,6 +152,7 @@ describe Note, models: true do
with([{
text: note1.note,
context: {
+ skip_project_check: false,
pipeline: :note,
cache_key: [note1, "note"],
project: note1.project,
@@ -150,6 +164,7 @@ describe Note, models: true do
with([{
text: note2.note,
context: {
+ skip_project_check: false,
pipeline: :note,
cache_key: [note2, "note"],
project: note2.project,
@@ -306,4 +321,70 @@ describe Note, models: true do
end
end
end
+
+ describe '#for_personal_snippet?' do
+ it 'returns false for a project snippet note' do
+ expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy
+ end
+
+ it 'returns true for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).for_personal_snippet?).to be_truthy
+ end
+ end
+
+ describe '#to_ability_name' do
+ it 'returns snippet for a project snippet note' do
+ expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet')
+ end
+
+ it 'returns personal_snippet for a personal snippet note' do
+ expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet')
+ end
+
+ it 'returns merge_request for an MR note' do
+ expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request')
+ end
+
+ it 'returns issue for an issue note' do
+ expect(build(:note_on_issue).to_ability_name).to eq('issue')
+ end
+
+ it 'returns issue for a commit note' do
+ expect(build(:note_on_commit).to_ability_name).to eq('commit')
+ end
+ end
+
+ describe '#cache_markdown_field' do
+ let(:html) { '<p>some html</p>'}
+
+ context 'note for a project snippet' do
+ let(:note) { build(:note_on_project_snippet) }
+
+ before do
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).
+ with(note, :note, { skip_project_check: false }).and_return(html)
+
+ note.save
+ end
+
+ it 'creates a note' do
+ expect(note.note_html).to eq(html)
+ end
+ end
+
+ context 'note for a personal snippet' do
+ let(:note) { build(:note_on_personal_snippet) }
+
+ before do
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).
+ with(note, :note, { skip_project_check: true }).and_return(html)
+
+ note.save
+ end
+
+ it 'creates a note' do
+ expect(note.note_html).to eq(html)
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e93a4e62244..8048e86fc3a 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -73,9 +73,7 @@ describe Project, models: true do
context 'after initialized' do
it "has a project_feature" do
- project = FactoryGirl.build(:project)
-
- expect(project.project_feature.present?).to be_present
+ expect(Project.new.project_feature).to be_present
end
end
@@ -129,7 +127,7 @@ describe Project, models: true do
end
describe 'validation' do
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
@@ -148,7 +146,7 @@ describe Project, models: true do
it { is_expected.to validate_presence_of(:repository_storage) }
it 'does not allow new projects beyond user limits' do
- project2 = build(:project)
+ project2 = build(:empty_project)
allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
expect(project2).not_to be_valid
expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
@@ -157,7 +155,7 @@ describe Project, models: true do
describe 'wiki path conflict' do
context "when the new path has been used by the wiki of other Project" do
it 'has an error on the name attribute' do
- new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
+ new_project = build_stubbed(:empty_project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
expect(new_project).not_to be_valid
expect(new_project.errors[:name].first).to eq('has already been taken')
@@ -166,8 +164,8 @@ describe Project, models: true do
context "when the new wiki path has been used by the path of other Project" do
it 'has an error on the name attribute' do
- project_with_wiki_suffix = create(:project, path: 'foo.wiki')
- new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
+ project_with_wiki_suffix = create(:empty_project, path: 'foo.wiki')
+ new_project = build_stubbed(:empty_project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
expect(new_project).not_to be_valid
expect(new_project.errors[:name].first).to eq('has already been taken')
@@ -176,7 +174,7 @@ describe Project, models: true do
end
context 'repository storages inclussion' do
- let(:project2) { build(:project, repository_storage: 'missing') }
+ let(:project2) { build(:empty_project, repository_storage: 'missing') }
before do
storages = { 'custom' => 'tmp/tests/custom_repositories' }
@@ -352,7 +350,7 @@ describe Project, models: true do
end
describe '#repository_storage_path' do
- let(:project) { create(:project, repository_storage: 'custom') }
+ let(:project) { create(:empty_project, repository_storage: 'custom') }
before do
FileUtils.mkdir('tmp/tests/custom_repositories')
@@ -412,7 +410,7 @@ describe Project, models: true do
describe 'last_activity methods' do
let(:timestamp) { 2.hours.ago }
# last_activity_at gets set to created_at upon creation
- let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
+ let(:project) { create(:empty_project, created_at: timestamp, updated_at: timestamp) }
describe 'last_activity' do
it 'alias last_activity to last_event' do
@@ -496,7 +494,7 @@ describe Project, models: true do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
- @project = create(:project, name: 'gitlabhq', namespace: @group)
+ @project = create(:empty_project, name: 'gitlabhq', namespace: @group)
end
it { expect(@project.to_param).to eq('gitlabhq') }
@@ -522,7 +520,7 @@ describe Project, models: true do
end
describe '#repository' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
it 'returns valid repo' do
expect(project.repository).to be_kind_of(Repository)
@@ -530,20 +528,22 @@ describe Project, models: true do
end
describe '#default_issues_tracker?' do
- let(:project) { create(:project) }
- let(:ext_project) { create(:redmine_project) }
-
it "is true if used internal tracker" do
+ project = build(:empty_project)
+
expect(project.default_issues_tracker?).to be_truthy
end
it "is false if used other tracker" do
- expect(ext_project.default_issues_tracker?).to be_falsey
+ # NOTE: The current nature of this factory requires persistence
+ project = create(:redmine_project)
+
+ expect(project.default_issues_tracker?).to be_falsey
end
end
describe '#external_issue_tracker' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:ext_project) { create(:redmine_project) }
context 'on existing projects with no value for has_external_issue_tracker' do
@@ -578,7 +578,7 @@ describe Project, models: true do
end
describe '#cache_has_external_issue_tracker' do
- let(:project) { create(:project, has_external_issue_tracker: nil) }
+ let(:project) { create(:empty_project, has_external_issue_tracker: nil) }
it 'stores true if there is any external_issue_tracker' do
services = double(:service, external_issue_trackers: [RedmineService.new])
@@ -600,9 +600,9 @@ describe Project, models: true do
end
describe '#has_wiki?' do
- let(:no_wiki_project) { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
- let(:wiki_enabled_project) { create(:project) }
- let(:external_wiki_project) { create(:project, has_external_wiki: true) }
+ let(:no_wiki_project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
+ let(:wiki_enabled_project) { create(:empty_project) }
+ let(:external_wiki_project) { create(:empty_project, has_external_wiki: true) }
it 'returns true if project is wiki enabled or has external wiki' do
expect(wiki_enabled_project).to have_wiki
@@ -612,7 +612,7 @@ describe Project, models: true do
end
describe '#external_wiki' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'with an active external wiki' do
before do
@@ -663,7 +663,7 @@ describe Project, models: true do
end
describe '#open_branches' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
project.protected_branches.create(name: 'master')
@@ -685,7 +685,7 @@ describe Project, models: true do
it 'counts stars from multiple users' do
user1 = create :user
user2 = create :user
- project = create :project, :public
+ project = create(:empty_project, :public)
expect(project.star_count).to eq(0)
@@ -707,8 +707,8 @@ describe Project, models: true do
it 'counts stars on the right project' do
user = create :user
- project1 = create :project, :public
- project2 = create :project, :public
+ project1 = create(:empty_project, :public)
+ project2 = create(:empty_project, :public)
expect(project1.star_count).to eq(0)
expect(project2.star_count).to eq(0)
@@ -740,7 +740,7 @@ describe Project, models: true do
end
describe '#avatar_type' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
it 'is true if avatar is image' do
project.update_attribute(:avatar, 'uploads/avatar.png')
@@ -756,7 +756,7 @@ describe Project, models: true do
describe '#avatar_url' do
subject { project.avatar_url }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
context 'When avatar file is uploaded' do
before do
@@ -791,7 +791,7 @@ describe Project, models: true do
end
describe '#pipeline_for' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let!(:pipeline) { create_pipeline }
shared_examples 'giving the correct pipeline' do
@@ -825,7 +825,7 @@ describe Project, models: true do
end
describe '#builds_enabled' do
- let(:project) { create :project }
+ let(:project) { create(:empty_project) }
subject { project.builds_enabled }
@@ -877,7 +877,7 @@ describe Project, models: true do
end
describe '.visible_to_user' do
- let!(:project) { create(:project, :private) }
+ let!(:project) { create(:empty_project, :private) }
let!(:user) { create(:user) }
subject { described_class.visible_to_user(user) }
@@ -975,7 +975,7 @@ describe Project, models: true do
end
describe '#visibility_level_allowed?' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:empty_project, :internal) }
context 'when checking on non-forked project' do
it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
@@ -984,8 +984,8 @@ describe Project, models: true do
end
context 'when checking on forked project' do
- let(:project) { create(:project, :internal) }
- let(:forked_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:empty_project, :internal) }
+ let(:forked_project) { create(:empty_project, forked_from_project: project) }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
@@ -994,7 +994,7 @@ describe Project, models: true do
end
describe '.search' do
- let(:project) { create(:project, description: 'kitten mittens') }
+ let(:project) { create(:empty_project, description: 'kitten mittens') }
it 'returns projects with a matching name' do
expect(described_class.search(project.name)).to eq([project])
@@ -1052,7 +1052,7 @@ describe Project, models: true do
end
describe '#rename_repo' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:gitlab_shell) { Gitlab::Shell.new }
before do
@@ -1102,7 +1102,7 @@ describe Project, models: true do
end
describe '#expire_caches_before_rename' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repo) { double(:repo, exists?: true) }
let(:wiki) { double(:wiki, exists?: true) }
@@ -1123,7 +1123,7 @@ describe Project, models: true do
end
describe '.search_by_title' do
- let(:project) { create(:project, name: 'kittens') }
+ let(:project) { create(:empty_project, name: 'kittens') }
it 'returns projects with a matching name' do
expect(described_class.search_by_title(project.name)).to eq([project])
@@ -1142,8 +1142,8 @@ describe Project, models: true 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 :empty_project, :private, group: private_group }
+ let(:internal_project) { create :empty_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 }
@@ -1155,7 +1155,7 @@ describe Project, models: true do
end
describe '#create_repository' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
before do
@@ -1197,7 +1197,7 @@ describe Project, models: true do
describe '#protected_branch?' do
context 'existing project' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
it 'returns true when the branch matches a protected branch via direct match' do
create(:protected_branch, project: project, name: "foo")
@@ -1381,7 +1381,7 @@ describe Project, models: true do
name: name)
end
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { create_pipeline }
context 'with many builds' do
@@ -1461,7 +1461,7 @@ describe Project, models: true do
end
context 'not forked' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
it 'schedules a RepositoryImportWorker job' do
expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
@@ -1472,19 +1472,19 @@ describe Project, models: true do
end
describe '#gitlab_project_import?' do
- subject(:project) { build(:project, import_type: 'gitlab_project') }
+ subject(:project) { build(:empty_project, import_type: 'gitlab_project') }
it { expect(project.gitlab_project_import?).to be true }
end
describe '#gitea_import?' do
- subject(:project) { build(:project, import_type: 'gitea') }
+ subject(:project) { build(:empty_project, import_type: 'gitea') }
it { expect(project.gitea_import?).to be true }
end
describe '#lfs_enabled?' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
shared_examples 'project overrides group' do
it 'returns true when enabled in project' do
@@ -1546,7 +1546,7 @@ describe Project, models: true do
end
describe '#change_head' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
it 'calls the before_change_head and after_change_head methods' do
expect(project.repository).to receive(:before_change_head)
@@ -1574,7 +1574,7 @@ describe Project, models: true do
end
describe '#pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
after do
project.reset_pushes_since_gc
@@ -1596,7 +1596,7 @@ describe Project, models: true do
end
describe '#increment_pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
after do
project.reset_pushes_since_gc
@@ -1610,7 +1610,7 @@ describe Project, models: true do
end
describe '#reset_pushes_since_gc' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
after do
project.reset_pushes_since_gc
@@ -1626,7 +1626,7 @@ describe Project, models: true do
end
describe '#environments_for' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
context 'tagged deployment' do
@@ -1678,7 +1678,7 @@ describe Project, models: true do
end
describe '#environments_recently_updated_on_branch' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
context 'when last deployment to environment is the most recent one' do
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 77403cc9eb0..ff29f6f66ba 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -107,7 +107,7 @@ describe ProjectStatistics, models: true do
describe '#update_repository_size' do
before do
- allow(project.repository).to receive(:size).and_return(12.megabytes)
+ allow(project.repository).to receive(:size).and_return(12)
statistics.update_repository_size
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
new file mode 100644
index 00000000000..f08935b6425
--- /dev/null
+++ b/spec/models/timelog_spec.rb
@@ -0,0 +1,10 @@
+require 'rails_helper'
+
+RSpec.describe Timelog, type: :model do
+ subject { build(:timelog) }
+
+ it { is_expected.to be_valid }
+
+ it { is_expected.to validate_presence_of(:time_spent) }
+ it { is_expected.to validate_presence_of(:user) }
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8b20ee81614..c45d0b9317b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -48,7 +48,7 @@ describe User, models: true do
describe '#project_members' do
it 'does not include project memberships for which user is a requester' do
user = create(:user)
- project = create(:project, :public, :access_requestable)
+ project = create(:empty_project, :public, :access_requestable)
project.request_access(user)
expect(user.project_members).to be_empty
@@ -386,13 +386,15 @@ describe User, models: true do
describe 'projects' do
before do
- @user = create :user
- @project = create :project, namespace: @user.namespace
- @project_2 = create :project, group: create(:group) # Grant MASTER access to the user
- @project_3 = create :project, group: create(:group) # Grant DEVELOPER access to the user
+ @user = create(:user)
- @project_2.team << [@user, :master]
- @project_3.team << [@user, :developer]
+ @project = create(:empty_project, namespace: @user.namespace)
+ @project_2 = create(:empty_project, group: create(:group)) do |project|
+ project.add_master(@user)
+ end
+ @project_3 = create(:empty_project, group: create(:group)) do |project|
+ project.add_developer(@user)
+ end
end
it { expect(@user.authorized_projects).to include(@project) }
@@ -435,7 +437,7 @@ describe User, models: true do
describe 'namespaced' do
before do
@user = create :user
- @project = create :project, namespace: @user.namespace
+ @project = create(:empty_project, namespace: @user.namespace)
end
it { expect(@user.several_namespaces?).to be_falsey }
@@ -517,7 +519,7 @@ describe User, models: true do
before do
User.delete_all
@user = create :user
- @project = create :project
+ @project = create(:empty_project)
end
it { expect(User.not_in_project(@project)).to include(@user, @project.owner) }
@@ -795,14 +797,14 @@ describe User, models: true do
describe '#avatar_type' do
let(:user) { create(:user) }
- it "is true if avatar is image" do
+ it 'is true if avatar is image' do
user.update_attribute(:avatar, 'uploads/avatar.png')
expect(user.avatar_type).to be_truthy
end
- it "is false if avatar is html page" do
+ it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
- expect(user.avatar_type).to eq(["only images allowed"])
+ expect(user.avatar_type).to eq(['only images allowed'])
end
end
@@ -924,11 +926,11 @@ describe User, models: true do
end
end
- describe "#starred?" do
- it "determines if user starred a project" do
+ describe '#starred?' do
+ it 'determines if user starred a project' do
user = create :user
- project1 = create :project, :public
- project2 = create :project, :public
+ project1 = create(:empty_project, :public)
+ project2 = create(:empty_project, :public)
expect(user.starred?(project1)).to be_falsey
expect(user.starred?(project2)).to be_falsey
@@ -951,10 +953,10 @@ describe User, models: true do
end
end
- describe "#toggle_star" do
- it "toggles stars" do
+ describe '#toggle_star' do
+ it 'toggles stars' do
user = create :user
- project = create :project, :public
+ project = create(:empty_project, :public)
expect(user.starred?(project)).to be_falsey
user.toggle_star(project)
@@ -964,39 +966,52 @@ describe User, models: true do
end
end
- describe "#sort" do
+ describe '#sort' do
before do
User.delete_all
@user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
+ @user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
end
- it "sorts users by the recent sign-in time" do
- expect(User.sort('recent_sign_in').first).to eq(@user)
+ context 'when sort by recent_sign_in' do
+ it 'sorts users by the recent sign-in time' do
+ expect(User.sort('recent_sign_in').first).to eq(@user)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('recent_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users by the oldest sign-in time" do
- expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ context 'when sort by oldest_sign_in' do
+ it 'sorts users by the oldest sign-in time' do
+ expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('oldest_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users in descending order by their creation time" do
+ it 'sorts users in descending order by their creation time' do
expect(User.sort('created_desc').first).to eq(@user)
end
- it "sorts users in ascending order by their creation time" do
- expect(User.sort('created_asc').first).to eq(@user1)
+ it 'sorts users in ascending order by their creation time' do
+ expect(User.sort('created_asc').first).to eq(@user2)
end
- it "sorts users by id in descending order when nil is passed" do
- expect(User.sort(nil).first).to eq(@user1)
+ it 'sorts users by id in descending order when nil is passed' do
+ expect(User.sort(nil).first).to eq(@user2)
end
end
describe "#contributed_projects" do
subject { create(:user) }
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project, forked_from_project: project3) }
- let!(:project3) { create(:project) }
+ let!(:project1) { create(:empty_project) }
+ let!(:project2) { create(:empty_project, forked_from_project: project3) }
+ let!(:project3) { create(:empty_project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) }
let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) }
@@ -1038,8 +1053,8 @@ describe User, models: true do
describe "#recent_push" do
subject { create(:user) }
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project, forked_from_project: project1) }
+ let!(:project1) { create(:project, :repository) }
+ let!(:project2) { create(:project, :repository, forked_from_project: project1) }
let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject)
end
@@ -1113,7 +1128,7 @@ describe User, models: true do
it "includes user's personal projects" do
user = create(:user)
- project = create(:project, :private, namespace: user.namespace)
+ project = create(:empty_project, :private, namespace: user.namespace)
expect(user.authorized_projects).to include(project)
end
@@ -1121,7 +1136,7 @@ describe User, models: true do
it "includes personal projects user has been given access to" do
user1 = create(:user)
user2 = create(:user)
- project = create(:project, :private, namespace: user1.namespace)
+ project = create(:empty_project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
@@ -1130,7 +1145,7 @@ describe User, models: true do
it "includes projects of groups user has been added to" do
group = create(:group)
- project = create(:project, group: group)
+ project = create(:empty_project, group: group)
user = create(:user)
group.add_developer(user)
@@ -1140,7 +1155,7 @@ describe User, models: true do
it "does not include projects of groups user has been removed from" do
group = create(:group)
- project = create(:project, group: group)
+ project = create(:empty_project, group: group)
user = create(:user)
member = group.add_developer(user)
@@ -1152,7 +1167,7 @@ describe User, models: true do
it "includes projects shared with user's group" do
user = create(:user)
- project = create(:project, :private)
+ project = create(:empty_project, :private)
group = create(:group)
group.add_reporter(user)
@@ -1164,7 +1179,7 @@ describe User, models: true do
it "does not include destroyed projects user had access to" do
user1 = create(:user)
user2 = create(:user)
- project = create(:project, :private, namespace: user1.namespace)
+ project = create(:empty_project, :private, namespace: user1.namespace)
project.team << [user2, Gitlab::Access::DEVELOPER]
expect(user2.authorized_projects).to include(project)
@@ -1175,7 +1190,7 @@ describe User, models: true do
it "does not include projects of destroyed groups user had access to" do
group = create(:group)
- project = create(:project, namespace: group)
+ project = create(:empty_project, namespace: group)
user = create(:user)
group.add_developer(user)
@@ -1190,14 +1205,9 @@ describe User, models: true do
let(:user) { create(:user) }
it 'includes projects for which the user access level is above or equal to reporter' do
- create(:project)
- reporter_project = create(:project)
- developer_project = create(:project)
- master_project = create(:project)
-
- reporter_project.team << [user, :reporter]
- developer_project.team << [user, :developer]
- master_project.team << [user, :master]
+ reporter_project = create(:empty_project) { |p| p.add_reporter(user) }
+ developer_project = create(:empty_project) { |p| p.add_developer(user) }
+ master_project = create(:empty_project) { |p| p.add_master(user) }
expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
expect(user.can?(:admin_issue, master_project)).to eq(true)
@@ -1206,10 +1216,8 @@ describe User, models: true do
end
it 'does not include for which the user access level is below reporter' do
- project = create(:project)
- guest_project = create(:project)
-
- guest_project.team << [user, :guest]
+ project = create(:empty_project)
+ guest_project = create(:empty_project) { |p| p.add_guest(user) }
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, guest_project)).to eq(false)
@@ -1217,15 +1225,14 @@ describe User, models: true do
end
it 'does not include archived projects' do
- project = create(:project)
- project.update_attributes(archived: true)
+ project = create(:empty_project, :archived)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
end
it 'does not include projects for which issues are disabled' do
- project = create(:project, issues_access_level: ProjectFeature::DISABLED)
+ project = create(:empty_project, issues_access_level: ProjectFeature::DISABLED)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
@@ -1241,7 +1248,7 @@ describe User, models: true do
end
context 'without any projects' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
it 'does not load' do
expect(user.ci_authorized_runners).to be_empty
@@ -1250,7 +1257,7 @@ describe User, models: true do
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
- let(:project) { create(:project, namespace: namespace) }
+ let(:project) { create(:empty_project, namespace: namespace) }
it 'loads' do
expect(user.ci_authorized_runners).to contain_exactly(runner)
@@ -1281,7 +1288,7 @@ describe User, models: true do
context 'with groups projects runners' do
let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
+ let(:project) { create(:empty_project, group: group) }
def add_user(access)
group.add_user(user, access)
@@ -1291,7 +1298,7 @@ describe User, models: true do
end
context 'with other projects runners' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
def add_user(access)
project.team << [user, access]
@@ -1321,8 +1328,8 @@ describe User, models: true do
end
describe '#projects_with_reporter_access_limited_to' do
- let(:project1) { create(:project) }
- let(:project2) { create(:project) }
+ let(:project1) { create(:empty_project) }
+ let(:project2) { create(:empty_project) }
let(:user) { create(:user) }
before do
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
new file mode 100644
index 00000000000..63acc0b68cd
--- /dev/null
+++ b/spec/policies/base_policy_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe BasePolicy, models: true do
+ let(:build) { Ci::Build.new }
+
+ describe '.class_for' do
+ it 'detects policy class based on the subject ancestors' do
+ expect(described_class.class_for(build)).to eq(Ci::BuildPolicy)
+ end
+
+ it 'detects policy class for a presented subject' do
+ presentee = Ci::BuildPresenter.new(build)
+
+ expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy)
+ end
+ end
+end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
new file mode 100644
index 00000000000..7a35da38b2b
--- /dev/null
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Ci::BuildPresenter do
+ let(:project) { create(:empty_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
+
+ describe '#initialize' do
+ it 'takes a build and optional params' do
+ expect { presenter }.not_to raise_error
+ end
+
+ it 'exposes build' do
+ expect(presenter.build).to eq(build)
+ end
+
+ it 'forwards missing methods to build' do
+ expect(presenter.ref).to eq('master')
+ end
+ end
+
+ describe '#erased_by_user?' do
+ it 'takes a build and optional params' do
+ expect(presenter).not_to be_erased_by_user
+ end
+ end
+
+ describe '#erased_by_name' do
+ context 'when build is not erased' do
+ before do
+ expect(presenter).to receive(:erased_by_user?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(presenter.erased_by_name).to be_nil
+ end
+ end
+
+ context 'when build is erased' do
+ before do
+ expect(presenter).to receive(:erased_by_user?).and_return(true)
+ expect(build).to receive(:erased_by).
+ and_return(double(:user, name: 'John Doe'))
+ end
+
+ it 'returns the name of the eraser' do
+ expect(presenter.erased_by_name).to eq('John Doe')
+ end
+ end
+ end
+
+ describe 'quack like a Ci::Build permission-wise' do
+ context 'user is not allowed' do
+ let(:project) { build_stubbed(:empty_project, public_builds: false) }
+
+ it 'returns false' do
+ expect(presenter.can?(nil, :read_build)).to be_falsy
+ end
+ end
+
+ context 'user is allowed' do
+ let(:project) { build_stubbed(:empty_project, :public) }
+
+ it 'returns true' do
+ expect(presenter.can?(nil, :read_build)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 1a771b3c87a..e487297748b 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -9,7 +9,7 @@ describe API::AccessRequests, api: true do
let(:stranger) { create(:user) }
let(:project) do
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
project.team << [developer, :developer]
project.team << [master, :master]
project.request_access(access_requester)
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 3019724f52e..c14c3cb1ce7 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -8,7 +8,7 @@ describe API::Boards, api: true do
let(:non_member) { create(:user) }
let(:guest) { create(:user) }
let(:admin) { create(:user, :admin) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
@@ -188,7 +188,7 @@ describe API::Boards, api: true do
context "when the user is project owner" do
let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 335efc4db6c..c1c7c0882de 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -152,8 +152,11 @@ describe API::CommitStatuses, api: true do
context 'with all optional parameters' do
before do
- optional_params = { state: 'success', context: 'coverage',
- ref: 'develop', target_url: 'url', description: 'test' }
+ optional_params = { state: 'success',
+ context: 'coverage',
+ ref: 'develop',
+ description: 'test',
+ target_url: 'http://gitlab.com/status' }
post api(post_url, developer), optional_params
end
@@ -164,12 +167,12 @@ describe API::CommitStatuses, api: true do
expect(json_response['status']).to eq('success')
expect(json_response['name']).to eq('coverage')
expect(json_response['ref']).to eq('develop')
- expect(json_response['target_url']).to eq('url')
expect(json_response['description']).to eq('test')
+ expect(json_response['target_url']).to eq('http://gitlab.com/status')
end
end
- context 'invalid status' do
+ context 'when status is invalid' do
before { post api(post_url, developer), state: 'invalid' }
it 'does not create commit status' do
@@ -177,7 +180,7 @@ describe API::CommitStatuses, api: true do
end
end
- context 'request without state' do
+ context 'when request without a state made' do
before { post api(post_url, developer) }
it 'does not create commit status' do
@@ -185,7 +188,7 @@ describe API::CommitStatuses, api: true do
end
end
- context 'invalid commit' do
+ context 'when commit SHA is invalid' do
let(:sha) { 'invalid_sha' }
before { post api(post_url, developer), state: 'running' }
@@ -193,6 +196,19 @@ describe API::CommitStatuses, api: true do
expect(response).to have_http_status(404)
end
end
+
+ context 'when target URL is an invalid address' do
+ before do
+ post api(post_url, developer), state: 'pending',
+ target_url: 'invalid url'
+ end
+
+ it 'responds with bad request status and validation errors' do
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['target_url'])
+ .to include 'must be a valid URL'
+ end
+ end
end
context 'reporter user' do
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index aabab8e6ae6..766234d7104 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -5,8 +5,8 @@ describe API::DeployKeys, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
@@ -73,19 +73,14 @@ describe API::DeployKeys, api: true do
post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' }
expect(response).to have_http_status(400)
- expect(json_response['message']['key']).to eq([
- 'can\'t be blank',
- 'is invalid'
- ])
+ expect(json_response['error']).to eq('key is missing')
end
it 'should not create a key without title' do
post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key'
expect(response).to have_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'can\'t be blank'
- ])
+ expect(json_response['error']).to eq('title is missing')
end
it 'should create new ssh key' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index b9d535bc314..8168b613766 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -5,7 +5,7 @@ describe API::Environments, api: true do
let(:user) { create(:user) }
let(:non_member) { create(:user) }
- let(:project) { create(:project, :private, namespace: user.namespace) }
+ let(:project) { create(:empty_project, :private, namespace: user.namespace) }
let!(:environment) { create(:environment, project: project) }
before do
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index e38d5745d44..df29099bc2f 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::Projects, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index e355d5e28bc..edbf0140583 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -10,9 +10,9 @@ describe API::Groups, api: true do
let(:admin) { create(:admin) }
let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
let!(:group2) { create(:group, :private) }
- let!(:project1) { create(:project, namespace: group1) }
- let!(:project2) { create(:project, namespace: group2) }
- let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+ let!(:project1) { create(:empty_project, namespace: group1) }
+ let!(:project2) { create(:empty_project, namespace: group2) }
+ let!(:project3) { create(:empty_project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
group1.add_owner(user1)
@@ -163,7 +163,7 @@ describe API::Groups, api: true do
describe "GET /groups/:id" do
context "when authenticated as user" do
it "returns one of user1's groups" do
- project = create(:project, namespace: group2, path: 'Foo')
+ project = create(:empty_project, namespace: group2, path: 'Foo')
create(:project_group_link, project: project, group: group1)
get api("/groups/#{group1.id}", user1)
@@ -287,7 +287,7 @@ describe API::Groups, api: true do
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
- expect(json_response.first['default_branch']).to be_present
+ expect(json_response.first['visibility_level']).to be_present
end
it "returns the group's projects with simple representation" do
@@ -297,11 +297,11 @@ describe API::Groups, api: true do
expect(json_response.length).to eq(2)
project_names = json_response.map { |proj| proj['name' ] }
expect(project_names).to match_array([project1.name, project3.name])
- expect(json_response.first['default_branch']).not_to be_present
+ expect(json_response.first['visibility_level']).not_to be_present
end
it 'filters the groups projects' do
- public_project = create(:project, :public, path: 'test1', group: group1)
+ public_project = create(:empty_project, :public, path: 'test1', group: group1)
get api("/groups/#{group1.id}/projects", user1), visibility: 'public'
@@ -462,7 +462,7 @@ describe API::Groups, api: true do
end
describe "POST /groups/:id/projects/:project_id" do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:project_path) { "#{project.namespace.path}%2F#{project.path}" }
before(:each) do
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index b8ee2293a33..a89676fec93 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -12,6 +12,7 @@ describe API::Helpers, api: true do
let(:params) { {} }
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) }
+ let(:header) { }
def set_env(user_or_token, identifier)
clear_env
@@ -46,7 +47,7 @@ describe API::Helpers, api: true do
allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
end
- def error!(message, status)
+ def error!(message, status, header)
raise Exception.new("#{status} - #{message}")
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 35644bd8cc9..91202244227 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -239,7 +239,7 @@ describe API::Internal, api: true do
end
context "blocked user" do
- let(:personal_project) { create(:project, namespace: user.namespace) }
+ let(:personal_project) { create(:empty_project, namespace: user.namespace) }
before do
user.block
@@ -265,7 +265,7 @@ describe API::Internal, api: true do
end
context "archived project" do
- let(:personal_project) { create(:project, namespace: user.namespace) }
+ let(:personal_project) { create(:empty_project, namespace: user.namespace) }
before do
project.team << [user, :developer]
@@ -337,8 +337,7 @@ describe API::Internal, api: true do
context 'ssh access has been disabled' do
before do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'http')
+ stub_application_setting(enabled_git_access_protocol: 'http')
end
it 'rejects the SSH push' do
@@ -360,8 +359,7 @@ describe API::Internal, api: true do
context 'http access has been disabled' do
before do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
end
it 'rejects the HTTP push' do
@@ -383,8 +381,7 @@ describe API::Internal, api: true do
context 'web actions are always allowed' do
it 'allows WEB push' do
- settings = ::ApplicationSetting.create_from_defaults
- settings.update_attribute(:enabled_git_access_protocol, 'ssh')
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
project.team << [user, :developer]
push(key, project, 'web')
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 12dd4bd83f7..62f1b8d7ca2 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -11,7 +11,7 @@ describe API::Issues, api: true do
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
author: user,
@@ -224,7 +224,7 @@ describe API::Issues, api: true do
describe "GET /groups/:id/issues" do
let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
let!(:group_closed_issue) do
create :closed_issue,
author: user,
@@ -1052,7 +1052,7 @@ describe API::Issues, api: true do
context "when the user is project owner" do
let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
delete api("/projects/#{project.id}/issues/#{issue.id}", owner)
@@ -1071,8 +1071,8 @@ describe API::Issues, api: true do
end
describe '/projects/:id/issues/:issue_id/move' do
- let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
- let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
+ let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
+ let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
it 'moves an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
@@ -1193,4 +1193,10 @@ describe API::Issues, api: true do
expect(response).to have_http_status(404)
end
end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { issue }
+
+ include_examples 'time tracking endpoints', 'issue'
+ end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index b29ce1ea25e..a8cd787f398 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -4,7 +4,7 @@ describe API::Labels, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:label1) { create(:label, title: 'label1', project: project) }
let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) }
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 2c94c86ccfa..9892e014cb9 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -9,7 +9,7 @@ describe API::Members, api: true do
let(:stranger) { create(:user) }
let(:project) do
- create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
project.team << [developer, :developer]
project.team << [master, :master]
project.request_access(access_requester)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index f032d1b683d..71a7994e544 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -6,7 +6,7 @@ describe API::MergeRequests, api: true do
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:non_member) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
@@ -308,8 +308,8 @@ describe API::MergeRequests, api: true do
context 'forked projects' do
let!(:user2) { create(:user) }
- let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
- let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
+ let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) }
before :each do |each|
fork_project.team << [user2, :reporter]
@@ -627,6 +627,17 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.first['id']).to eq(issue.id)
end
+
+ it 'returns 403 if the user has no access to the merge request' do
+ project = create(:empty_project, :private)
+ merge_request = create(:merge_request, :simple, source_project: project)
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+
+ expect(response).to have_http_status(403)
+ end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
@@ -648,6 +659,15 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
@@ -669,6 +689,21 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'Time tracking' do
+ let(:issuable) { merge_request }
+
+ include_examples 'time tracking endpoints', 'merge_request'
end
def mr_with_later_created_and_updated_at_time
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 028f93c8561..0353ebea9e5 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Notes, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, :public, namespace: user.namespace) }
+ let!(:project) { create(:empty_project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
let!(:snippet) { create(:project_snippet, project: project, author: user) }
@@ -14,12 +14,12 @@ describe API::Notes, api: true do
# For testing the cross-reference of a private issue in a public issue
let(:private_user) { create(:user) }
let(:private_project) do
- create(:project, namespace: private_user.namespace).
+ create(:empty_project, namespace: private_user.namespace).
tap { |p| p.team << [private_user, :master] }
end
let(:private_issue) { create(:issue, project: private_project) }
- let(:ext_proj) { create(:project, :public) }
+ let(:ext_proj) { create(:empty_project, :public) }
let(:ext_issue) { create(:issue, project: ext_proj) }
let!(:cross_reference_note) do
@@ -264,8 +264,20 @@ describe API::Notes, api: true do
end
end
+ context 'when user does not have access to read the noteable' do
+ it 'responds with 404' do
+ project = create(:empty_project, :private) { |p| p.add_guest(user) }
+ issue = create(:issue, :confidential, project: project)
+
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+ body: 'Foo'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
context 'when user does not have access to create noteable' do
- let(:private_issue) { create(:issue, project: create(:project, :private)) }
+ let(:private_issue) { create(:issue, project: create(:empty_project, :private)) }
##
# We are posting to project user has access to, but we use issue id
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index 8691a81420f..39d3afcb78f 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -5,7 +5,7 @@ describe API::NotificationSettings, api: true do
let(:user) { create(:user) }
let!(:group) { create(:group) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
describe "GET /notification_settings" do
it "returns global notification settings for the current user" do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 36fbcf088e7..f4973d71088 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -4,7 +4,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user3) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let!(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let!(:hook) do
create(:project_hook,
:all_events_enabled,
@@ -204,7 +204,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
test_user = create(:user)
- other_project = create(:project)
+ other_project = create(:empty_project)
other_project.team << [test_user, :master]
delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index cdb16b4c46b..cc5c532de83 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -8,8 +8,8 @@ describe API::Projects, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :master, user: user, project: project) }
let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
@@ -32,7 +32,7 @@ describe API::Projects, api: true do
access_level: ProjectMember::MASTER)
end
let(:project4) do
- create(:project,
+ create(:empty_project,
name: 'third_project',
path: 'third_project',
creator_id: user4.id,
@@ -252,7 +252,7 @@ describe API::Projects, api: true do
end
end
- let!(:public_project) { create(:project, :public) }
+ let!(:public_project) { create(:empty_project, :public) }
before do
project
project2
@@ -283,7 +283,7 @@ describe API::Projects, api: true do
end
describe 'GET /projects/starred' do
- let(:public_project) { create(:project, :public) }
+ let(:public_project) { create(:empty_project, :public) }
before do
project_member2
@@ -583,7 +583,7 @@ describe API::Projects, api: true do
describe 'GET /projects/:id' do
context 'when unauthenticated' do
it 'returns the public projects' do
- public_project = create(:project, :public)
+ public_project = create(:empty_project, :public)
get api("/projects/#{public_project.id}")
@@ -665,7 +665,7 @@ describe API::Projects, api: true do
it 'handles users with dots' do
dot_user = create(:user, username: 'dot.user')
- project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+ project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace)
get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
expect(response).to have_http_status(200)
@@ -711,7 +711,7 @@ describe API::Projects, api: true do
end
context 'group project' do
- let(:project2) { create(:project, group: create(:group)) }
+ let(:project2) { create(:empty_project, group: create(:group)) }
before { project2.group.add_owner(user) }
@@ -756,7 +756,7 @@ describe API::Projects, api: true do
context 'when unauthenticated' do
it_behaves_like 'project events response' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:current_user) { nil }
end
end
@@ -807,7 +807,7 @@ describe API::Projects, api: true do
context 'when unauthenticated' do
it_behaves_like 'project users response' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:current_user) { nil }
end
end
@@ -921,11 +921,11 @@ describe API::Projects, api: true do
end
describe :fork_admin do
- let(:project_fork_target) { create(:project) }
- let(:project_fork_source) { create(:project, :public) }
+ let(:project_fork_target) { create(:empty_project) }
+ let(:project_fork_source) { create(:empty_project, :public) }
describe 'POST /projects/:id/fork/:forked_from_id' do
- let(:new_project_fork_source) { create(:project, :public) }
+ let(:new_project_fork_source) { create(:empty_project, :public) }
it "is not available for non admin users" do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
@@ -966,7 +966,7 @@ describe API::Projects, api: true do
end
context 'when users belong to project group' do
- let(:project_fork_target) { create(:project, group: create(:group)) }
+ let(:project_fork_target) { create(:empty_project, group: create(:group)) }
before do
project_fork_target.group.add_owner user
@@ -1121,7 +1121,6 @@ describe API::Projects, api: true do
it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
let(:current_user) { user }
end
-
end
context 'when authenticated as a different user' do
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 99414270be6..f2d81a28cb8 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -7,8 +7,8 @@ describe API::Runners, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
let!(:shared_runner) { create(:ci_runner, :shared) }
let!(:unused_specific_runner) { create(:ci_runner) }
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 39c9e0505d1..776dc655650 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -6,7 +6,7 @@ describe API::Services, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
- let(:project) {create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
Service.available_services_names.each do |service|
describe "PUT /projects/:id/services/#{service.dasherize}" do
@@ -16,6 +16,15 @@ describe API::Services, api: true do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
expect(response).to have_http_status(200)
+
+ current_service = project.services.first
+ event = current_service.event_names.empty? ? "foo" : current_service.event_names.first
+ state = current_service[event] || false
+
+ put api("/projects/#{project.id}/services/#{dashed_service}?#{event}=#{!state}", user), service_attrs
+
+ expect(response).to have_http_status(200)
+ expect(project.services.first[event]).not_to eq(state) unless event == "foo"
end
it "returns if required fields missing" do
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 887a2ba5b84..56dc017ce54 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe API::Todos, api: true do
include ApiHelpers
- let(:project_1) { create(:project) }
- let(:project_2) { create(:project) }
+ let(:project_1) { create(:empty_project) }
+ let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
let(:author_2) { create(:user) }
let(:john_doe) { create(:user, username: 'john_doe') }
@@ -183,12 +183,25 @@ describe API::Todos, api: true do
expect(response.status).to eq(404)
end
+
+ it 'returns an error if the issuable is not accessible' do
+ guest = create(:user)
+ project_1.team << [guest, :guest]
+
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
+
+ if issuable_type == 'merge_requests'
+ expect(response).to have_http_status(403)
+ else
+ expect(response).to have_http_status(404)
+ end
+ end
end
describe 'POST :id/issuable_type/:issueable_id/todo' do
context 'for an issue' do
it_behaves_like 'an issuable', 'issues' do
- let(:issuable) { create(:issue, author: author_1, project: project_1) }
+ let(:issuable) { create(:issue, :confidential, author: author_1, project: project_1) }
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 67ec3168679..cd01283b655 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -15,7 +15,7 @@ describe API::Triggers do
let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
describe 'POST /projects/:project_id/trigger' do
- let!(:project2) { create(:empty_project) }
+ let!(:project2) { create(:project) }
let(:options) do
{
token: trigger_token
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 7435f320607..769f04c5057 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -5,7 +5,7 @@ describe API::Variables, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:empty_project, creator_id: user.id) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
let!(:variable) { create(:ci_variable, project: project) }
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 3b5dc98e4d5..270c23e3f19 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -4,7 +4,8 @@ describe Ci::API::Builds do
include ApiHelpers
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
- let(:project) { FactoryGirl.create(:empty_project) }
+ let(:project) { FactoryGirl.create(:empty_project, shared_runners_enabled: false) }
+ let(:last_update) { nil }
describe "Builds API for runners" do
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
@@ -16,6 +17,8 @@ describe Ci::API::Builds do
describe "POST /builds/register" do
let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
+ let!(:last_update) { }
+ let!(:new_update) { }
before do
stub_container_registry_config(enabled: false)
@@ -24,7 +27,31 @@ describe Ci::API::Builds do
shared_examples 'no builds available' do
context 'when runner sends version in User-Agent' do
context 'for stable version' do
- it { expect(response).to have_http_status(204) }
+ it 'gives 204 and set X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header).to have_key('X-GitLab-Last-Update')
+ end
+ end
+
+ context 'when last_update is up-to-date' do
+ let(:last_update) { runner.ensure_runner_queue_value }
+
+ it 'gives 204 and set the same X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header['X-GitLab-Last-Update'])
+ .to eq(last_update)
+ end
+ end
+
+ context 'when last_update is outdated' do
+ let(:last_update) { runner.ensure_runner_queue_value }
+ let(:new_update) { runner.tick_runner_queue }
+
+ it 'gives 204 and set a new X-GitLab-Last-Update' do
+ expect(response).to have_http_status(204)
+ expect(response.header['X-GitLab-Last-Update'])
+ .to eq(new_update)
+ end
end
context 'for beta version' do
@@ -49,6 +76,7 @@ describe Ci::API::Builds do
register_builds info: { platform: :darwin }
expect(response).to have_http_status(201)
+ expect(response.headers).not_to have_key('X-GitLab-Last-Update')
expect(json_response['sha']).to eq(build.sha)
expect(runner.reload.platform).to eq("darwin")
expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
@@ -119,10 +147,10 @@ describe Ci::API::Builds do
end
context 'for shared runner' do
- let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
+ let!(:runner) { create(:ci_runner, :shared, token: "SharedRunner") }
before do
- register_builds shared_runner.token
+ register_builds(runner.token)
end
it_behaves_like 'no builds available'
@@ -224,7 +252,9 @@ describe Ci::API::Builds do
end
def register_builds(token = runner.token, **params)
- post ci_api("/builds/register"), params.merge(token: token), { 'User-Agent' => user_agent }
+ new_params = params.merge(token: token, last_update: last_update)
+
+ post ci_api("/builds/register"), new_params, { 'User-Agent' => user_agent }
end
end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index e0368e6001f..72978846e93 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'cycle analytics events' do
+ include ApiHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, public_builds: false) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
@@ -20,19 +22,19 @@ describe 'cycle analytics events' do
it 'lists the issue events' do
get namespace_project_cycle_analytics_issue_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
-
- first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+ first_issue_iid = project.issues.sort(:created_desc).pluck(:iid).first.to_s
+ expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
it 'lists the plan events' do
get namespace_project_cycle_analytics_plan_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
+ first_mr_short_sha = project.merge_requests.sort(:created_asc).first.commits.first.short_id
- expect(json_response['events'].first['short_sha']).to eq(MergeRequest.last.commits.first.short_id)
+ expect(json_response['events']).not_to be_empty
+ expect(json_response['events'].first['short_sha']).to eq(first_mr_short_sha)
end
it 'lists the code events' do
@@ -40,7 +42,7 @@ describe 'cycle analytics events' do
expect(json_response['events']).not_to be_empty
- first_mr_iid = project.merge_requests.order(id: :desc).pluck(:iid).first.to_s
+ first_mr_iid = project.merge_requests.sort(:created_desc).pluck(:iid).first.to_s
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
@@ -49,17 +51,15 @@ describe 'cycle analytics events' do
get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json)
expect(json_response['events']).not_to be_empty
-
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the review events' do
get namespace_project_cycle_analytics_review_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
-
- first_mr_iid = MergeRequest.order(created_at: :desc).pluck(:iid).first.to_s
+ first_mr_iid = project.merge_requests.sort(:created_desc).pluck(:iid).first.to_s
+ expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
@@ -67,35 +67,32 @@ describe 'cycle analytics events' do
get namespace_project_cycle_analytics_staging_path(project.namespace, project, format: :json)
expect(json_response['events']).not_to be_empty
-
expect(json_response['events'].first['date']).not_to be_empty
end
it 'lists the production events' do
get namespace_project_cycle_analytics_production_path(project.namespace, project, format: :json)
- expect(json_response['events']).not_to be_empty
-
- first_issue_iid = Issue.order(created_at: :desc).pluck(:iid).first.to_s
+ first_issue_iid = project.issues.sort(:created_desc).pluck(:iid).first.to_s
+ expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
context 'specific branch' do
it 'lists the test events' do
- branch = MergeRequest.first.source_branch
+ branch = project.merge_requests.first.source_branch
get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json, branch: branch)
expect(json_response['events']).not_to be_empty
-
expect(json_response['events'].first['date']).not_to be_empty
end
end
context 'with private project and builds' do
before do
- ProjectMember.first.update(access_level: Gitlab::Access::GUEST)
+ project.members.first.update(access_level: Gitlab::Access::GUEST)
end
it 'does not list the test events' do
@@ -118,10 +115,6 @@ describe 'cycle analytics events' do
end
end
- def json_response
- JSON.parse(response.body)
- end
-
def create_cycle
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb
new file mode 100644
index 00000000000..f9951826683
--- /dev/null
+++ b/spec/serializers/analytics_stage_serializer_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe AnalyticsStageSerializer do
+ let(:serializer) do
+ described_class
+ .new.represent(resource)
+ end
+
+ let(:json) { serializer.as_json }
+ let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) }
+
+ before do
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12)
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
+ end
+
+ it 'it generates payload for single object' do
+ expect(json).to be_kind_of Hash
+ end
+
+ it 'contains important elements of AnalyticsStage' do
+ expect(json).to include(:title, :description, :value)
+ end
+end
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
new file mode 100644
index 00000000000..7a84c8b0b40
--- /dev/null
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe AnalyticsSummarySerializer do
+ let(:serializer) do
+ described_class
+ .new.represent(resource)
+ end
+
+ let(:json) { serializer.as_json }
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:resource) do
+ Gitlab::CycleAnalytics::Summary::Issue.new(project: double,
+ from: 1.day.ago,
+ current_user: user)
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12)
+ end
+
+ it 'it generates payload for single object' do
+ expect(json).to be_kind_of Hash
+ end
+
+ it 'contains important elements of AnalyticsStage' do
+ expect(json).to include(:title, :value)
+ end
+end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index b19464c7117..ccb72973f9c 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -134,5 +134,17 @@ describe PipelineEntity do
expect(subject).not_to have_key(:yaml_errors)
end
end
+
+ context 'when pipeline ref is empty' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ before do
+ allow(pipeline).to receive(:ref).and_return(nil)
+ end
+
+ it 'does not generate branch path' do
+ expect(subject[:ref][:path]).to be_nil
+ end
+ end
end
end
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
new file mode 100644
index 00000000000..f01a388b895
--- /dev/null
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Ci::UpdateBuildQueueService, :services do
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when updating specific runners' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'when there are runner that can pick build' do
+ before { build.project.runners << runner }
+
+ it 'ticks runner queue value' do
+ expect { subject.execute(build) }
+ .to change { runner.ensure_runner_queue_value }
+ end
+ end
+
+ context 'when there are no runners 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 }
+ end
+ end
+ end
+
+ context 'when updating shared runners' do
+ let(:runner) { create(:ci_runner, :shared) }
+
+ context 'when there are 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 are no runners that can pick build' do
+ before { build.tag_list = [:docker] }
+
+ it 'does not tick runner queue value' do
+ expect { subject.execute(build) }
+ .not_to change { runner.ensure_runner_queue_value }
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7e3705983fb..314ea670a71 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -106,23 +106,46 @@ describe MergeRequests::RefreshService, services: true do
context 'push to fork repo source branch' do
let(:refresh_service) { service.new(@fork_project, @user) }
- before do
- allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- reload_mrs
- end
- it 'executes hooks with update action' do
- expect(refresh_service).to have_received(:execute_hooks).
- with(@fork_merge_request, 'update', @oldrev)
+ context 'open fork merge request' do
+ before do
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+ end
+
+ it 'executes hooks with update action' do
+ expect(refresh_service).to have_received(:execute_hooks).
+ with(@fork_merge_request, 'update', @oldrev)
+ end
+
+ it { expect(@merge_request.notes).to be_empty }
+ it { expect(@merge_request).to be_open }
+ it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
+ it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ context 'closed fork merge request' do
+ before do
+ @fork_merge_request.close!
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+ end
+
+ it 'do not execute hooks with update action' do
+ expect(refresh_service).not_to have_received(:execute_hooks)
+ end
+
+ it { expect(@merge_request.notes).to be_empty }
+ it { expect(@merge_request).to be_open }
+ it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@fork_merge_request).to be_closed }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
+ end
end
context 'push to fork repo target branch' do
@@ -237,6 +260,70 @@ describe MergeRequests::RefreshService, services: true do
end
end
+ context 'marking the merge request as work in progress' do
+ let(:refresh_service) { service.new(@project, @user) }
+ before do
+ allow(refresh_service).to receive(:execute_hooks)
+ end
+
+ it 'marks the merge request as work in progress from fixup commits' do
+ fixup_merge_request = create(:merge_request,
+ source_project: @project,
+ source_branch: 'wip',
+ target_branch: 'master',
+ target_project: @project)
+ commits = fixup_merge_request.commits
+ oldrev = commits.last.id
+ newrev = commits.first.id
+
+ refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
+ fixup_merge_request.reload
+
+ expect(fixup_merge_request.work_in_progress?).to eq(true)
+ expect(fixup_merge_request.notes.last.note).to match(
+ /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
+ )
+ end
+
+ it 'references the commit that caused the Work in Progress status' do
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+
+ allow(refresh_service).to receive(:find_new_commits)
+ refresh_service.instance_variable_set("@commits", [
+ instance_double(
+ Commit,
+ id: 'aaaaaaa',
+ short_id: 'aaaaaaa',
+ title: 'Fix issue',
+ work_in_progress?: false
+ ),
+ instance_double(
+ Commit,
+ id: 'bbbbbbb',
+ short_id: 'bbbbbbb',
+ title: 'fixup! Fix issue',
+ work_in_progress?: true,
+ to_reference: 'bbbbbbb'
+ ),
+ instance_double(
+ Commit,
+ id: 'ccccccc',
+ short_id: 'ccccccc',
+ title: 'fixup! Fix issue',
+ work_in_progress?: true,
+ to_reference: 'ccccccc'
+ ),
+ ])
+
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
+ reload_mrs
+
+ expect(@merge_request.notes.last.note).to eq(
+ "marked as a **Work In Progress** from bbbbbbb"
+ )
+ end
+ end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index b0cc3ce5f5a..9c92a5080c6 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -15,39 +15,45 @@ describe Notes::CreateService, services: true do
context "valid params" do
it 'returns a valid note' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
end
it 'returns a persisted note' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_persisted
end
it 'note has valid content' do
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note.note).to eq(opts[:note])
end
+ it 'note belongs to the correct project' do
+ note = described_class.new(project, user, opts).execute
+
+ expect(note.project).to eq(project)
+ end
+
it 'TodoService#new_note is called' do
- note = build(:note)
- allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+ note = build(:note, project: project)
+ allow(Note).to receive(:new).with(opts) { note }
expect_any_instance_of(TodoService).to receive(:new_note).with(note, user)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
it 'enqueues NewNoteWorker' do
- note = build(:note, id: 999)
- allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+ note = build(:note, id: 999, project: project)
+ allow(Note).to receive(:new).with(opts) { note }
expect(NewNoteWorker).to receive(:perform_async).with(note.id)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
@@ -75,6 +81,27 @@ describe Notes::CreateService, services: true do
end
end
end
+
+ describe 'personal snippet note' do
+ subject { described_class.new(nil, user, params).execute }
+
+ let(:snippet) { create(:personal_snippet) }
+ let(:params) do
+ { note: 'comment', noteable_type: 'Snippet', noteable_id: snippet.id }
+ end
+
+ it 'returns a valid note' do
+ expect(subject).to be_valid
+ end
+
+ it 'returns a persisted note' do
+ expect(subject).to be_persisted
+ end
+
+ it 'note has valid content' do
+ expect(subject.note).to eq(params[:note])
+ end
+ end
end
describe "award emoji" do
@@ -88,7 +115,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.name).to eq('smile')
@@ -100,7 +127,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
- note = Notes::CreateService.new(project, user, opts).execute
+ note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.note).to eq(opts[:note])
@@ -115,7 +142,7 @@ describe Notes::CreateService, services: true do
expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
- Notes::CreateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
end
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
index 960b5cd5e6f..1a64c8bbf00 100644
--- a/spec/services/notes/slash_commands_service_spec.rb
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -86,6 +86,18 @@ describe Notes::SlashCommandsService, services: true do
expect(note.noteable).to be_open
end
end
+
+ describe '/spend' do
+ let(:note_text) { '/spend 1h' }
+
+ it 'updates the spent time on the noteable' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq ''
+ expect(note.noteable.time_spent).to eq(3600)
+ end
+ end
end
describe 'note with command & text' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f3e80ac22a0..bfbee7ca35f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -269,6 +269,55 @@ describe NotificationService, services: true do
end
end
+ context 'personal snippet note' do
+ let(:snippet) { create(:personal_snippet, :public, author: @u_snippet_author) }
+ let(:note) { create(:note_on_personal_snippet, noteable: snippet, note: '@mentioned note', author: @u_note_author) }
+
+ before do
+ @u_watcher = create_global_setting_for(create(:user), :watch)
+ @u_participant = create_global_setting_for(create(:user), :participating)
+ @u_disabled = create_global_setting_for(create(:user), :disabled)
+ @u_mentioned = create_global_setting_for(create(:user, username: 'mentioned'), :mention)
+ @u_mentioned_level = create_global_setting_for(create(:user, username: 'participator'), :mention)
+ @u_note_author = create(:user, username: 'note_author')
+ @u_snippet_author = create(:user, username: 'snippet_author')
+ @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
+
+ reset_delivered_emails!
+ end
+
+ let!(:notes) do
+ [
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_watcher),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled),
+ create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author),
+ ]
+ end
+
+ describe '#new_note' do
+ it 'notifies the participants' do
+ notification.new_note(note)
+
+ # it emails participants
+ should_email(@u_watcher)
+ should_email(@u_participant)
+ should_email(@u_watcher)
+ should_email(@u_snippet_author)
+
+ # it emails mentioned users
+ should_email(@u_mentioned)
+
+ # it does not email participants with mention notification level
+ should_not_email(@u_mentioned_level)
+
+ # it does not email note author
+ should_not_email(@u_note_author)
+ end
+ end
+ end
+
context 'commit note' do
let(:project) { create(:project, :public) }
let(:note) { create(:note_on_commit, project: project) }
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index ffcf02d2c56..66fc8fc360b 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -211,6 +211,46 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'estimate command' do
+ it 'populates time_estimate: 3600 if content contains /estimate 1h' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(time_estimate: 3600)
+ end
+ end
+
+ shared_examples 'spend command' do
+ it 'populates spend_time: 3600 if content contains /spend 1h' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(spend_time: { duration: 3600, user: developer })
+ end
+ end
+
+ shared_examples 'spend command with negative time' do
+ it 'populates spend_time: -1800 if content contains /spend -30m' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(spend_time: { duration: -1800, user: developer })
+ end
+ end
+
+ shared_examples 'remove_estimate command' do
+ it 'populates time_estimate: 0 if content contains /remove_estimate' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(time_estimate: 0)
+ end
+ end
+
+ shared_examples 'remove_time_spent command' do
+ it 'populates spend_time: :reset if content contains /remove_time_spent' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(spend_time: { duration: :reset, user: developer })
+ end
+ end
+
shared_examples 'empty command' do
it 'populates {} if content contains an unsupported command' do
_, updates = service.execute(content, issuable)
@@ -518,6 +558,51 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ it_behaves_like 'estimate command' do
+ let(:content) { '/estimate 1h' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/estimate' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/estimate abc' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'spend command' do
+ let(:content) { '/spend 1h' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'spend command with negative time' do
+ let(:content) { '/spend -30m' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/spend' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/spend abc' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'remove_estimate command' do
+ let(:content) { '/remove_estimate' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'remove_time_spent command' do
+ let(:content) { '/remove_time_spent' }
+ let(:issuable) { issue }
+ end
+
context 'when current_user cannot :admin_issue' do
let(:visitor) { create(:user) }
let(:issue) { create(:issue, project: project, author: visitor) }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 0e8adb68721..9f5a0ac4ec6 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -740,4 +740,92 @@ describe SystemNoteService, services: true do
expect(note.note).to include(issue.to_reference)
end
end
+
+ describe '.change_time_estimate' do
+ subject { described_class.change_time_estimate(noteable, project, author) }
+
+ it_behaves_like 'a system note'
+
+ context 'with a time estimate' do
+ it 'sets the note text' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h"
+ end
+ end
+
+ context 'without a time estimate' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "Removed time estimate on this issue"
+ end
+ end
+ end
+
+ describe '.change_time_spent' do
+ # We need a custom noteable in order to the shared examples to be green.
+ let(:noteable) do
+ mr = create(:merge_request, source_project: project)
+ mr.spend_time(duration: 360000, user: author)
+ mr.save!
+ mr
+ end
+
+ subject do
+ described_class.change_time_spent(noteable, project, author)
+ end
+
+ it_behaves_like 'a system note'
+
+ context 'when time was added' do
+ it 'sets the note text' do
+ spend_time!(277200)
+
+ expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request"
+ end
+ end
+
+ context 'when time was subtracted' do
+ it 'sets the note text' do
+ spend_time!(-277200)
+
+ expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request"
+ end
+ end
+
+ context 'when time was removed' do
+ it 'sets the note text' do
+ spend_time!(:reset)
+
+ expect(subject.note).to eq "Removed time spent on this merge request"
+ end
+ end
+
+ def spend_time!(seconds)
+ noteable.spend_time(duration: seconds, user: author)
+ noteable.save!
+ end
+ end
+
+ describe '.add_merge_request_wip_from_commit' do
+ let(:noteable) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ subject do
+ described_class.add_merge_request_wip_from_commit(
+ noteable,
+ project,
+ author,
+ noteable.diff_head_commit
+ )
+ end
+
+ it_behaves_like 'a system note'
+
+ it "posts the 'marked as a Work In Progress from commit' system note" do
+ expect(subject.note).to match(
+ /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
+ )
+ end
+ end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index ed55791d24e..13d584a8975 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -469,6 +469,13 @@ describe TodoService, services: true do
should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
end
+
+ it 'creates a pending todo for merge_user' do
+ mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin)
+ service.merge_request_build_failed(mr_unassigned)
+
+ should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ end
end
describe '#merge_request_push' do
@@ -482,6 +489,15 @@ describe TodoService, services: true do
end
end
+ describe '#merge_request_became_unmergeable' do
+ it 'creates a pending todo for a merge_user' do
+ mr_unassigned.update(merge_when_build_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)
+ end
+ end
+
describe '#mark_todo' do
it 'creates a todo from a merge request' do
service.mark_todo(mr_unassigned, author)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6ee3307512d..f78899134d5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -2,6 +2,7 @@ require './spec/simplecov_env'
SimpleCovEnv.start!
ENV["RAILS_ENV"] ||= 'test'
+ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
new file mode 100644
index 00000000000..210cd5817e0
--- /dev/null
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -0,0 +1,132 @@
+shared_examples 'an unauthorized API user' do
+ it { is_expected.to eq(403) }
+end
+
+shared_examples 'time tracking endpoints' do |issuable_name|
+ issuable_collection_name = issuable_name.pluralize
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "sets the time estimate for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['human_time_estimate']).to eq('1w')
+ end
+
+ describe 'updating the current estimate' do
+ before do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+ end
+
+ context 'when duration has a bad format' do
+ it 'does not modify the original estimate' do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(issuable.reload.human_time_estimate).to eq('1w')
+ end
+ end
+
+ context 'with a valid duration' do
+ it 'updates the estimate' do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
+
+ expect(response).to have_http_status(200)
+ expect(issuable.reload.human_time_estimate).to eq('3w 1h')
+ end
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets the time estimate for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['time_estimate']).to eq(0)
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
+ context 'with an unauthorized user' do
+ subject do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
+ duration: '2h'
+ end
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "add spent time for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '2h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['human_total_time_spent']).to eq('2h')
+ end
+
+ context 'when subtracting time' do
+ it 'subtracts time of the total spent time' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['total_time_spent']).to eq(3600)
+ end
+ end
+
+ context 'when time to subtract is greater than the total spent time' do
+ it 'does not modify the total time spent' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1w'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets spent time for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(0)
+ end
+ end
+
+ describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
+ it "returns the time stats for #{issuable_name}" do
+ issuable.update_attributes!(spend_time: { duration: 1800, user: user },
+ time_estimate: 3600)
+
+ get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(1800)
+ expect(json_response['time_estimate']).to eq(3600)
+ end
+ end
+end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index 8e19a6c92e2..35b40d73191 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -2,7 +2,6 @@
# Note: The ABC size is large here because we have a method generating test cases with
# multiple nested contexts. This shouldn't count as a violation.
-
module CycleAnalyticsHelpers
module TestGeneration
# Generate the most common set of specs that all cycle analytics phases need to have.
@@ -51,7 +50,7 @@ module CycleAnalyticsHelpers
end
median_time_difference = time_differences.sort[2]
- expect(subject.send(phase)).to be_within(5).of(median_time_difference)
+ expect(subject[phase].median).to be_within(5).of(median_time_difference)
end
context "when the data belongs to another project" do
@@ -83,7 +82,7 @@ module CycleAnalyticsHelpers
# Turn off the stub before checking assertions
allow(self).to receive(:project).and_call_original
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
@@ -106,7 +105,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -126,7 +125,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
end
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -145,7 +144,7 @@ module CycleAnalyticsHelpers
post_fn[self, data] if post_fn
end
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -153,7 +152,7 @@ module CycleAnalyticsHelpers
context "when none of the start / end conditions are matched" do
it "returns nil" do
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 49867aa5cc4..a3724b801b3 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do
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' do
+ is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
+ is_expected.to have_header 'List-Unsubscribe', /mailto/
+ is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/
+ end
+
+ it { is_expected.to have_body_text /unsubscribe/ }
+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' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
- is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
+ is_expected.not_to have_header 'List-Unsubscribe', /mailto/
+ is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index ad1c783df4d..1b6c33248c9 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -33,6 +33,30 @@ shared_examples 'a Taskable' do
end
end
+ describe 'with nested tasks' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ - [ ] Task a
+ - [x] Task a.1
+ - [ ] Task a.2
+ - [ ] Task b
+
+ 1. [ ] Task 1
+ 1. [ ] Task 1.1
+ 1. [ ] Task 1.2
+ 1. [x] Task 2
+ 1. [x] Task 2.1
+ EOT
+ end
+
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('3 of')
+ expect(subject.task_status).to match('9 tasks completed')
+ expect(subject.task_status_short).to match('3/')
+ expect(subject.task_status_short).to match('9 tasks')
+ end
+ end
+
describe 'with an incomplete task' do
before do
subject.description = <<-EOT.strip_heredoc
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 4cf81be3adc..90f1a9c8798 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -35,7 +35,8 @@ module TestEnv
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
- 'deleted-image-test' => '6c17798'
+ 'deleted-image-test' => '6c17798',
+ 'wip' => 'b9238ee'
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
new file mode 100644
index 00000000000..02657684b57
--- /dev/null
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -0,0 +1,82 @@
+shared_examples 'issuable time tracker' do
+ it 'renders the sidebar component empty state' do
+ page.within '.time-tracking-no-tracking-pane' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'updates the sidebar component when estimate is added' do
+ submit_time('/estimate 3w 1d 1h')
+
+ page.within '.time-tracking-estimate-only-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'updates the sidebar component when spent is added' do
+ submit_time('/spend 3w 1d 1h')
+
+ page.within '.time-tracking-spend-only-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'shows the comparison when estimate and spent are added' do
+ submit_time('/estimate 3w 1d 1h')
+ submit_time('/spend 3w 1d 1h')
+
+ page.within '.time-tracking-comparison-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'updates the sidebar component when estimate is removed' do
+ submit_time('/estimate 3w 1d 1h')
+ submit_time('/remove_estimate')
+
+ page.within '#issuable-time-tracker' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'updates the sidebar component when spent is removed' do
+ submit_time('/spend 3w 1d 1h')
+ submit_time('/remove_time_spent')
+
+ page.within '#issuable-time-tracker' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'shows the help state when icon is clicked' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+ expect(page).to have_content 'Track time with slash commands'
+ expect(page).to have_content 'Learn more'
+ end
+ end
+
+ it 'hides the help state when close icon is clicked' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+ find('.close-help-button').click
+
+ expect(page).not_to have_content 'Track time with slash commands'
+ expect(page).not_to have_content 'Learn more'
+ end
+ end
+
+ it 'displays the correct help url' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+
+ expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')
+ end
+ end
+end
+
+def submit_time(slash_command)
+ fill_in 'note[note]', with: slash_command
+ click_button 'Comment'
+ wait_for_ajax
+end
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
index 5ea020f313c..643b161cdf4 100644
--- a/spec/teaspoon_env.rb
+++ b/spec/teaspoon_env.rb
@@ -166,7 +166,7 @@ Teaspoon.configure do |config|
# Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
# default excludes assets from vendor, gems and support libraries.
- coverage.ignore = [%r{vendor/}, %r{spec/}]
+ coverage.ignore = [%r{vendor/}, %r{spec/javascripts/(?!helpers/)}]
# Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
# aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
new file mode 100644
index 00000000000..36dfc539b3b
--- /dev/null
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -0,0 +1,76 @@
+# Explaination on the scripts:
+# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
+image: registry.gitlab.com/gitlab-examples/kubernetes-deploy
+
+variables:
+ # Application deployment domain
+ KUBE_DOMAIN: domain.example.com
+
+stages:
+ - build
+ - test
+ - review
+ - staging
+ - production
+
+build:
+ stage: build
+ script:
+ - command build
+ only:
+ - branches
+
+production:
+ stage: production
+ variables:
+ CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: production
+ url: http://production.$KUBE_DOMAIN
+ when: manual
+ only:
+ - master
+
+staging:
+ stage: staging
+ variables:
+ CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: staging
+ url: http://staging.$KUBE_DOMAIN
+ only:
+ - master
+
+review:
+ stage: review
+ variables:
+ CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ script:
+ - command deploy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
+
+stop_review:
+ stage: review
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - command destroy
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+ when: manual
+ only:
+ - branches
+ except:
+ - master