summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--.rubocop_todo.yml4
-rw-r--r--CHANGELOG.md130
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile30
-rw-r--r--Gemfile.lock20
-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/application.js1
-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.js27
-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.js.es619
-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.js710
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js79
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js145
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js60
-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/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.es666
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js.es644
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es653
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es6120
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js7
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6105
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6199
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es6208
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es683
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es645
-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.js30
-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.es64
-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.es610
-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.js23
-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_tabs.js.es69
-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.js24
-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.js10
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es630
-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.js26
-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/visibility_select.js.es627
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es61
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es612
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es658
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es626
-rw-r--r--app/assets/javascripts/zen_mode.js4
-rw-r--r--app/assets/stylesheets/framework/animations.scss74
-rw-r--r--app/assets/stylesheets/framework/avatar.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss115
-rw-r--r--app/assets/stylesheets/framework/header.scss13
-rw-r--r--app/assets/stylesheets/framework/layout.scss15
-rw-r--r--app/assets/stylesheets/framework/mobile.scss6
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss20
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss160
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/milestone.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss1
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss10
-rw-r--r--app/assets/stylesheets/pages/profile.scss15
-rw-r--r--app/assets/stylesheets/pages/projects.scss30
-rw-r--r--app/assets/stylesheets/pages/search.scss14
-rw-r--r--app/controllers/admin/application_settings_controller.rb99
-rw-r--r--app/controllers/admin/groups_controller.rb8
-rw-r--r--app/controllers/admin/users_controller.rb41
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb4
-rw-r--r--app/controllers/concerns/service_params.rb79
-rw-r--r--app/controllers/dashboard/milestones_controller.rb4
-rw-r--r--app/controllers/groups_controller.rb8
-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/merge_requests_controller.rb14
-rw-r--r--app/controllers/projects/notes_controller.rb3
-rw-r--r--app/controllers/projects/services_controller.rb4
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb18
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/finders/issuable_finder.rb54
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb6
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb9
-rw-r--r--app/helpers/merge_requests_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb11
-rw-r--r--app/helpers/todos_helper.rb3
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/ci/build.rb11
-rw-r--r--app/models/ci/runner.rb33
-rw-r--r--app/models/commit.rb14
-rw-r--r--app/models/commit_status.rb6
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/milestoneish.rb7
-rw-r--r--app/models/concerns/presentable.rb7
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/reactive_caching.rb46
-rw-r--r--app/models/concerns/reactive_service.rb10
-rw-r--r--app/models/concerns/time_trackable.rb72
-rw-r--r--app/models/concerns/valid_attribute.rb10
-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/forked_project_link.rb4
-rw-r--r--app/models/generic_commit_status.rb10
-rw-r--r--app/models/key.rb7
-rw-r--r--app/models/merge_request.rb18
-rw-r--r--app/models/merge_request_diff.rb20
-rw-r--r--app/models/project.rb24
-rw-r--r--app/models/project_services/bamboo_service.rb47
-rw-r--r--app/models/project_services/buildkite_service.rb23
-rw-r--r--app/models/project_services/chat_notification_service.rb14
-rw-r--r--app/models/project_services/ci_service.rb27
-rw-r--r--app/models/project_services/drone_ci_service.rb76
-rw-r--r--app/models/project_services/mattermost_service.rb2
-rw-r--r--app/models/project_services/teamcity_service.rb73
-rw-r--r--app/models/project_statistics.rb3
-rw-r--r--app/models/repository.rb5
-rw-r--r--app/models/timelog.rb6
-rw-r--r--app/models/todo.rb8
-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/build_action_entity.rb2
-rw-r--r--app/serializers/commit_entity.rb8
-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/refresh_service.rb19
-rw-r--r--app/services/merge_requests/update_service.rb15
-rw-r--r--app/services/notes/create_service.rb5
-rw-r--r--app/services/notes/slash_commands_service.rb4
-rw-r--r--app/services/projects/update_service.rb6
-rw-r--r--app/services/slash_commands/interpret_service.rb60
-rw-r--r--app/services/system_note_service.rb57
-rw-r--r--app/services/todo_service.rb30
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb41
-rw-r--r--app/views/admin/application_settings/_form.html.haml17
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml10
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml2
-rw-r--r--app/views/profiles/show.html.haml3
-rw-r--r--app/views/projects/_visibility_select.html.haml7
-rw-r--r--app/views/projects/branches/_branch.html.haml1
-rw-r--r--app/views/projects/builds/_sidebar.html.haml4
-rw-r--r--app/views/projects/builds/show.html.haml6
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml2
-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/diffs/_diffs.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/edit.html.haml110
-rw-r--r--app/views/projects/empty.html.haml11
-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/index.html.haml6
-rw-r--r--app/views/projects/issues/show.html.haml4
-rw-r--r--app/views/projects/mattermosts/_team_selection.html.haml11
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml8
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml10
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml3
-rw-r--r--app/views/projects/notes/_form.html.haml1
-rw-r--r--app/views/projects/pipelines/index.html.haml1
-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/stage/_graph.html.haml2
-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/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_no_password.html.haml4
-rw-r--r--app/views/shared/_no_ssh.html.haml4
-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.haml141
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml18
-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/milestones/_milestone.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml3
-rw-r--r--app/workers/build_queue_worker.rb10
-rw-r--r--app/workers/reactive_caching_worker.rb4
-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/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/24820-buttons-in-the-branches-page-are-stacking-on-top-of-each-other-depending-on-the-selected-filter.yml4
-rw-r--r--changelogs/unreleased/24876-page-jumps-to-wrong-position-when-clicking-a-comment-anchor.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/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/25985-combine-members-and-groups-settings-pages.yml5
-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/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/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-make-icon-buttons-accessible-via-keyboard.yml4
-rw-r--r--changelogs/unreleased/26446-access-download-artifacts-via-keyboard.yml5
-rw-r--r--changelogs/unreleased/26504-mr-discussion-btn.yml4
-rw-r--r--changelogs/unreleased/26785-fix-droplab-in-ie-11-v1.yml4
-rw-r--r--changelogs/unreleased/7898-fixes-issue-boards-list-colored-top-border-visual-glitch.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/badge-color-on-white-bg.yml4
-rw-r--r--changelogs/unreleased/clipboard-button-commit-sha.yml3
-rw-r--r--changelogs/unreleased/didemacet-ci-lint-page.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/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/filename-to-file-path.yml3
-rw-r--r--changelogs/unreleased/fix-api-deprecation.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-copy-issues-empty-state.yml4
-rw-r--r--changelogs/unreleased/fix-light-hr-in-descriptions.yml4
-rw-r--r--changelogs/unreleased/fix-timezone-due-date-picker.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_22664.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/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/re-style-issue-new-branch.yml3
-rw-r--r--changelogs/unreleased/recaptcha_500.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/single-edit-comment-widget-2.yml5
-rw-r--r--changelogs/unreleased/support-google-cloud-storage-backups.yml4
-rw-r--r--changelogs/unreleased/update-gitlab-markup-gem.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--config/application.rb4
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--config/routes/project.rb3
-rw-r--r--db/migrate/20161117114805_remove_undeleted_groups.rb96
-rw-r--r--db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb12
-rw-r--r--db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb12
-rw-r--r--db/migrate/20161223034433_add_estimate_to_issuables_ce.rb15
-rw-r--r--db/migrate/20161223034646_create_timelogs_ce.rb20
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb57
-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/20170106142508_fill_authorized_projects.rb30
-rw-r--r--db/schema.rb20
-rw-r--r--doc/README.md8
-rw-r--r--doc/administration/img/integration/plantuml-example.pngbin0 -> 33034 bytes
-rw-r--r--doc/administration/integration/plantuml.md87
-rw-r--r--doc/administration/monitoring/performance/introduction.md5
-rw-r--r--doc/administration/monitoring/performance/prometheus.md102
-rw-r--r--doc/api/README.md8
-rw-r--r--doc/api/issues.md143
-rw-r--r--doc/api/merge_requests.md139
-rw-r--r--doc/api/settings.md10
-rw-r--r--doc/ci/git_submodules.md13
-rw-r--r--doc/ci/yaml/README.md35
-rw-r--r--doc/development/frontend.md78
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/oauth_provider.md8
-rw-r--r--doc/integration/omniauth.md10
-rw-r--r--doc/project_services/project_services.md1
-rw-r--r--doc/project_services/slack_slash_commands.md2
-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/raketasks/backup_restore.md4
-rw-r--r--doc/user/project/slash_commands.md5
-rw-r--r--doc/workflow/README.md1
-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/admin/groups.feature49
-rw-r--r--features/admin/users.feature65
-rw-r--r--features/dashboard/active_tab.feature24
-rw-r--r--features/dashboard/archived_projects.feature17
-rw-r--r--features/dashboard/group.feature13
-rw-r--r--features/dashboard/help.feature9
-rw-r--r--features/project/active_tab.feature6
-rw-r--r--features/project/issues/filter_labels.feature28
-rw-r--r--features/project/issues/issues.feature56
-rw-r--r--features/steps/admin/groups.rb143
-rw-r--r--features/steps/admin/users.rb167
-rw-r--r--features/steps/dashboard/active_tab.rb5
-rw-r--r--features/steps/dashboard/archived_projects.rb26
-rw-r--r--features/steps/dashboard/dashboard.rb6
-rw-r--r--features/steps/dashboard/group.rb25
-rw-r--r--features/steps/dashboard/help.rb21
-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.rb6
-rw-r--r--features/steps/shared/project.rb18
-rw-r--r--features/steps/user.rb2
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/entities.rb9
-rw-r--r--lib/api/helpers.rb8
-rw-r--r--lib/api/issues.rb51
-rw-r--r--lib/api/merge_requests.rb22
-rw-r--r--lib/api/projects.rb10
-rw-r--r--lib/api/settings.rb6
-rw-r--r--lib/api/time_tracking_endpoints.rb114
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb8
-rw-r--r--lib/ci/api/builds.rb9
-rw-r--r--lib/gitlab/asciidoc.rb12
-rw-r--r--lib/gitlab/checks/change_access.rb4
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb1
-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/current_settings.rb1
-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/git/blame.rb4
-rw-r--r--lib/gitlab/git/blob.rb3
-rw-r--r--lib/gitlab/git/repository.rb8
-rw-r--r--lib/gitlab/gon_helper.rb1
-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/metrics/rack_middleware.rb15
-rw-r--r--lib/gitlab/redis.rb2
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/time_tracking_formatter.rb34
-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/tasks/dev.rake5
-rw-r--r--lib/tasks/gitlab/git.rake8
-rw-r--r--lib/tasks/gitlab/update_templates.rake2
-rw-r--r--public/robots.txt5
-rwxr-xr-xscripts/notify_slack.sh2
-rw-r--r--spec/controllers/admin/services_controller_spec.rb26
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb2
-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/merge_requests_controller_spec.rb68
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb48
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb20
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/factories/projects.rb8
-rw-r--r--spec/factories/timelogs.rb9
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/features/admin/admin_groups_spec.rb113
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_users_spec.rb173
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/cycle_analytics_spec.rb1
-rw-r--r--spec/features/dashboard/active_tab_spec.rb46
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb28
-rw-r--r--spec/features/dashboard/group_spec.rb20
-rw-r--r--spec/features/dashboard/help_spec.rb17
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb29
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb91
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb188
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb160
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb134
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb242
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb222
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb804
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb88
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb39
-rw-r--r--spec/features/issues/reset_filters_spec.rb89
-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/diffs_spec.rb14
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb27
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb (renamed from spec/features/issues/filter_by_labels_spec.rb)26
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb (renamed from spec/features/issues/filter_issues_spec.rb)143
-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/reset_filters_spec.rb96
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb45
-rw-r--r--spec/features/merge_requests/wip_message_spec.rb63
-rw-r--r--spec/features/projects/builds_spec.rb28
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb3
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb61
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/features/projects/project_settings_spec.rb10
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb85
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb47
-rw-r--r--spec/features/search_spec.rb8
-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/snippets/create_snippet_spec.rb14
-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/fixtures/config/redis_config_with_env.yml2
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb15
-rw-r--r--spec/javascripts/.eslintrc4
-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_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.es6134
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es659
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6104
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6104
-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.es648
-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.es624
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js.es625
-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.js9
-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/ci/gitlab_ci_yaml_processor_spec.rb13
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb4
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb17
-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/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/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/metrics/rack_middleware_spec.rb11
-rw-r--r--spec/lib/gitlab/redis_spec.rb16
-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/migrations/fill_authorized_projects_spec.rb18
-rw-r--r--spec/migrations/remove_dot_git_from_usernames.rb29
-rw-r--r--spec/migrations/remove_dot_git_from_usernames_spec.rb57
-rw-r--r--spec/models/build_spec.rb1338
-rw-r--r--spec/models/ci/build_spec.rb1306
-rw-r--r--spec/models/ci/runner_spec.rb56
-rw-r--r--spec/models/commit_spec.rb28
-rw-r--r--spec/models/commit_status_spec.rb19
-rw-r--r--spec/models/concerns/issuable_spec.rb38
-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_diff_spec.rb26
-rw-r--r--spec/models/merge_request_spec.rb152
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb149
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb77
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb72
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb126
-rw-r--r--spec/models/project_spec.rb149
-rw-r--r--spec/models/project_statistics_spec.rb2
-rw-r--r--spec/models/repository_spec.rb26
-rw-r--r--spec/models/timelog_spec.rb10
-rw-r--r--spec/models/user_spec.rb84
-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.rb4
-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.rb4
-rw-r--r--spec/requests/api/issues_spec.rb98
-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.rb12
-rw-r--r--spec/requests/api/notes_spec.rb8
-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.rb44
-rw-r--r--spec/requests/api/runners_spec.rb4
-rw-r--r--spec/requests/api/settings_spec.rb16
-rw-r--r--spec/requests/api/todos_spec.rb4
-rw-r--r--spec/requests/api/triggers_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb9
-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/build_action_entity_spec.rb4
-rw-r--r--spec/serializers/commit_entity_spec.rb2
-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.rb64
-rw-r--r--spec/services/merge_requests/update_service_spec.rb93
-rw-r--r--spec/services/notes/create_service_spec.rb11
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb12
-rw-r--r--spec/services/projects/update_service_spec.rb158
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb154
-rw-r--r--spec/services/system_note_service_spec.rb88
-rw-r--r--spec/services/todo_service_spec.rb16
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb40
-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/reactive_caching_helpers.rb30
-rw-r--r--spec/support/seed_helper.rb10
-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/gitignore/Android.gitignore2
-rw-r--r--vendor/gitignore/Autotools.gitignore15
-rw-r--r--vendor/gitignore/CMake.gitignore1
l---------[-rw-r--r--]vendor/gitignore/Clojure.gitignore14
-rw-r--r--vendor/gitignore/CodeIgniter.gitignore5
-rw-r--r--vendor/gitignore/CommonLisp.gitignore14
-rw-r--r--vendor/gitignore/Coq.gitignore29
-rw-r--r--vendor/gitignore/Dart.gitignore10
-rw-r--r--vendor/gitignore/Elisp.gitignore6
-rw-r--r--vendor/gitignore/Elixir.gitignore1
-rw-r--r--vendor/gitignore/Global/Emacs.gitignore5
-rw-r--r--vendor/gitignore/Global/IPythonNotebook.gitignore2
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore1
-rw-r--r--vendor/gitignore/Global/SublimeText.gitignore3
-rw-r--r--vendor/gitignore/Global/Vim.gitignore6
-rw-r--r--vendor/gitignore/Global/VisualStudioCode.gitignore1
-rw-r--r--vendor/gitignore/Global/Windows.gitignore3
-rw-r--r--vendor/gitignore/Go.gitignore2
-rw-r--r--vendor/gitignore/Java.gitignore3
-rw-r--r--vendor/gitignore/Laravel.gitignore2
-rw-r--r--vendor/gitignore/Maven.gitignore3
-rw-r--r--vendor/gitignore/Node.gitignore4
-rw-r--r--vendor/gitignore/Perl.gitignore38
-rw-r--r--vendor/gitignore/Python.gitignore1
-rw-r--r--vendor/gitignore/Symfony.gitignore5
-rw-r--r--vendor/gitignore/TeX.gitignore22
-rw-r--r--vendor/gitignore/VisualStudio.gitignore5
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml5
-rw-r--r--vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/Go.gitlab-ci.yml37
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml92
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml76
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml2
858 files changed, 16473 insertions, 6095 deletions
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/.rubocop_todo.yml b/.rubocop_todo.yml
index 6d4d7170fe8..d581610162f 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -62,7 +62,7 @@ Lint/UnusedMethodArgument:
# Offense count: 93
# Configuration parameters: CountComments.
Metrics/BlockLength:
- Max: 288
+ Enabled: false
# Offense count: 3
# Cop supports --auto-correct.
@@ -125,7 +125,7 @@ RSpec/MessageSpies:
# Offense count: 3036
RSpec/MultipleExpectations:
- Max: 37
+ Enabled: false
# Offense count: 2133
RSpec/NamedSubject:
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 a8141abafc2..83ba5d31b92 100644
--- a/Gemfile
+++ b/Gemfile
@@ -99,18 +99,19 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
-gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
-gem 'gitlab-markup', '~> 1.5.1'
-gem 'redcarpet', '~> 3.3.3'
-gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~> 4.2'
-gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~> 0.5.0'
-gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
-gem 'rouge', '~> 2.0'
-gem 'truncato', '~> 0.7.8'
+gem 'html-pipeline', '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
+gem 'gitlab-markup', '~> 1.5.1'
+gem 'redcarpet', '~> 3.3.3'
+gem 'RedCloth', '~> 4.3.2'
+gem 'rdoc', '~> 4.2'
+gem 'org-ruby', '~> 0.9.12'
+gem 'creole', '~> 0.5.0'
+gem 'wikicloth', '0.8.1'
+gem 'asciidoctor', '~> 1.5.2'
+gem 'asciidoctor-plantuml', '0.0.6'
+gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -251,10 +252,9 @@ 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 'rerun', '~> 0.11.0'
gem 'bullet', '~> 5.2.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
gem 'web-console', '~> 2.0'
@@ -285,7 +285,7 @@ group :development, :test do
gem 'minitest', '~> 5.7.0'
# Generate Fake data
- gem 'ffaker', '~> 2.0.0'
+ gem 'ffaker', '~> 2.4'
gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index bdd591e008a..104e6444803 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -54,6 +54,8 @@ GEM
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
+ asciidoctor-plantuml (0.0.6)
+ asciidoctor (~> 1.5)
ast (2.3.0)
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
@@ -86,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)
@@ -196,7 +198,7 @@ GEM
faraday_middleware-multi_json (0.0.6)
faraday_middleware
multi_json
- ffaker (2.0.0)
+ ffaker (2.4.0)
ffi (1.9.10)
flay (2.6.1)
ruby_parser (~> 3.0)
@@ -405,9 +407,6 @@ GEM
xml-simple
licensee (8.0.0)
rugged (>= 0.24b)
- listen (3.0.5)
- rb-fsevent (>= 0.9.3)
- rb-inotify (>= 0.9)
little-plugger (1.1.4)
logging (2.1.0)
little-plugger (~> 1.1)
@@ -578,9 +577,6 @@ GEM
rainbow (2.1.0)
raindrops (0.17.0)
rake (10.5.0)
- rb-fsevent (0.9.6)
- rb-inotify (0.9.5)
- ffi (>= 0.5.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2)
@@ -609,8 +605,6 @@ GEM
redis-store (1.2.0)
redis (>= 2.2)
request_store (1.3.1)
- rerun (0.11.0)
- listen (~> 3.0)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rest-client (2.0.0)
@@ -841,6 +835,7 @@ DEPENDENCIES
allocations (~> 1.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
+ asciidoctor-plantuml (= 0.0.6)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -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)
@@ -874,7 +869,7 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
- ffaker (~> 2.0.0)
+ ffaker (~> 2.4)
flay (~> 2.6.1)
fog-aws (~> 0.9)
fog-core (~> 1.40)
@@ -965,7 +960,6 @@ DEPENDENCIES
redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1)
request_store (~> 1.3)
- rerun (~> 0.11.0)
responders (~> 2.0)
rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
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/application.js b/app/assets/javascripts/application.js
index e43afbb4cc9..f0615481ed2 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -58,6 +58,7 @@
/*= require_directory ./extensions */
/*= require_directory ./lib/utils */
/*= require_directory ./u2f */
+/*= require_directory ./droplab */
/*= require_directory . */
/*= require fuzzaldrin-plus */
/*= require es6-promise.auto */
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 bc13c46443a..0df84234520 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,10 +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-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';
this.Build = (function() {
Build.interval = null;
@@ -26,7 +27,7 @@
this.$autoScrollStatus = $('#autoscroll-status');
this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
this.$upBuildTrace = $('#up-build-trace');
- this.$downBuildTrace = $('#down-build-trace');
+ this.$downBuildTrace = $(DOWN_BUILD_TRACE);
this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
@@ -69,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
@@ -84,13 +85,16 @@
};
Build.prototype.getInitialBuildTrace = function() {
- var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'];
return $.ajax({
url: this.buildUrl,
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
+ if (window.location.hash === DOWN_BUILD_TRACE) {
+ $("html,body").scrollTop(this.$buildTrace.height());
+ }
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
@@ -105,6 +109,8 @@
dataType: "json",
success: (function(_this) {
return function(log) {
+ var pageUrl;
+
if (log.state) {
_this.state = log.state;
}
@@ -116,7 +122,12 @@
}
return _this.checkAutoscroll();
} else if (log.status !== _this.buildStatus) {
- return Turbolinks.visit(_this.pageUrl);
+ pageUrl = _this.pageUrl;
+ if (_this.$autoScrollStatus.data('state') === 'enabled') {
+ pageUrl += DOWN_BUILD_TRACE;
+ }
+
+ return Turbolinks.visit(pageUrl);
}
};
})(this)
@@ -142,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
@@ -280,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.js.es6 b/app/assets/javascripts/diff.js.es6
index 9cf33e62958..5e1a4c948aa 100644
--- a/app/assets/javascripts/diff.js.es6
+++ b/app/assets/javascripts/diff.js.es6
@@ -20,7 +20,7 @@
.on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
- this.highlighSelectedLine();
+ this.openAnchoredDiff();
}
handleClickUnfold(e) {
@@ -61,13 +61,22 @@
$.get(link, params, response => $target.parent().replaceWith(response));
}
- openAnchoredDiff(anchoredDiff, cb) {
- const diffTitle = $(`#file-path-${anchoredDiff}`);
+ openAnchoredDiff(cb) {
+ const locationHash = gl.utils.getLocationHash();
+ const anchoredDiff = locationHash && locationHash.split('_')[0];
+
+ if (!anchoredDiff) return;
+
+ const diffTitle = $(`#${anchoredDiff}`);
const diffFile = diffTitle.closest('.diff-file');
const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) {
- diffFile.singleFileDiff(true, cb);
- } else {
+ const clickTarget = $('.file-title, .click-to-expand', diffFile);
+ diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
+ this.highlighSelectedLine();
+ if (cb) cb();
+ });
+ } else if (cb) {
cb();
}
}
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 54f13e328bd..dcf67a8fd68 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 */
@@ -84,6 +84,9 @@
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
Issuable.init();
new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
@@ -369,7 +372,5 @@
};
return Dispatcher;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js
new file mode 100644
index 00000000000..c79f0230951
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab.js
@@ -0,0 +1,710 @@
+/* eslint-disable */
+// Determine where to place this
+if (typeof Object.assign != 'function') {
+ Object.assign = function (target, varArgs) { // .length of function is 2
+ 'use strict';
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++) {
+ var nextSource = arguments[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (var nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+var DATA_TRIGGER = 'data-dropdown-trigger';
+var DATA_DROPDOWN = 'data-dropdown';
+
+module.exports = {
+ DATA_TRIGGER: DATA_TRIGGER,
+ DATA_DROPDOWN: DATA_DROPDOWN,
+}
+
+},{}],2:[function(require,module,exports){
+// Custom event support for IE
+if ( typeof CustomEvent === "function" ) {
+ module.exports = CustomEvent;
+} else {
+ require('./window')(function(w){
+ var CustomEvent = function ( event, params ) {
+ params = params || { bubbles: false, cancelable: false, detail: undefined };
+ var evt = document.createEvent( 'CustomEvent' );
+ evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
+ return evt;
+ }
+ CustomEvent.prototype = w.Event.prototype;
+
+ w.CustomEvent = CustomEvent;
+ });
+ module.exports = CustomEvent;
+}
+
+},{"./window":11}],3:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var utils = require('./utils');
+
+var DropDown = function(list) {
+ this.hidden = true;
+ this.list = list;
+ this.items = [];
+ this.getItems();
+ this.initTemplateString();
+ this.addEvents();
+ this.initialState = list.innerHTML;
+};
+
+Object.assign(DropDown.prototype, {
+ getItems: function() {
+ this.items = [].slice.call(this.list.querySelectorAll('li'));
+ 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');
+
+ if(selected) {
+ e.preventDefault();
+ this.hide();
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: this,
+ selected: selected,
+ data: e.target.dataset,
+ },
+ });
+ this.list.dispatchEvent(listEvent);
+ }
+ },
+
+ addEvents: function() {
+ this.clickWrapper = this.clickEvent.bind(this);
+ // event delegation.
+ this.list.addEventListener('click', this.clickWrapper);
+ },
+
+ toggle: function() {
+ if(this.hidden) {
+ this.show();
+ } else {
+ this.hide();
+ }
+ },
+
+ setData: function(data) {
+ this.data = data;
+ this.render(data);
+ },
+
+ addData: function(data) {
+ this.data = (this.data || []).concat(data);
+ this.render(this.data);
+ },
+
+ // call render manually on data;
+ render: function(data){
+ // debugger
+ // empty the list first
+ var templateString = this.templateString;
+ var newChildren = [];
+ var toAppend;
+
+ newChildren = (data ||[]).map(function(dat){
+ var html = utils.t(templateString, dat);
+ var template = document.createElement('div');
+ template.innerHTML = html;
+
+ // Help set the image src template
+ var imageTags = template.querySelectorAll('img[data-src]');
+ // debugger
+ for(var i = 0; i < imageTags.length; i++) {
+ var imageTag = imageTags[i];
+ imageTag.src = imageTag.getAttribute('data-src');
+ imageTag.removeAttribute('data-src');
+ }
+
+ if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){
+ template.firstChild.style.display = 'none'
+ }else{
+ template.firstChild.style.display = 'block';
+ }
+ return template.firstChild.outerHTML;
+ });
+ toAppend = this.list.querySelector('ul[data-dynamic]');
+ if(toAppend) {
+ toAppend.innerHTML = newChildren.join('');
+ } else {
+ this.list.innerHTML = newChildren.join('');
+ }
+ },
+
+ show: function() {
+ // debugger
+ this.list.style.display = 'block';
+ this.hidden = false;
+ },
+
+ hide: function() {
+ // debugger
+ this.list.style.display = 'none';
+ this.hidden = true;
+ },
+
+ destroy: function() {
+ this.hide();
+ this.list.removeEventListener('click', this.clickWrapper);
+ }
+});
+
+module.exports = DropDown;
+
+},{"./custom_event_polyfill":2,"./utils":10}],4:[function(require,module,exports){
+require('./window')(function(w){
+ module.exports = function(deps) {
+ deps = deps || {};
+ var window = deps.window || w;
+ var document = deps.document || window.document;
+ var CustomEvent = deps.CustomEvent || require('./custom_event_polyfill');
+ var HookButton = deps.HookButton || require('./hook_button');
+ var HookInput = deps.HookInput || require('./hook_input');
+ var utils = deps.utils || require('./utils');
+ var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+
+ var DropLab = function(hook){
+ if (!(this instanceof DropLab)) return new DropLab(hook);
+ this.ready = false;
+ this.hooks = [];
+ this.queuedData = [];
+ this.config = {};
+ this.loadWrapper;
+ if(typeof hook !== 'undefined'){
+ this.addHook(hook);
+ }
+ };
+
+
+ Object.assign(DropLab.prototype, {
+ load: function() {
+ this.loadWrapper();
+ },
+
+ loadWrapper: function(){
+ var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']'));
+ this.addHooks(dropdownTriggers).init();
+ },
+
+ addData: function () {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_addData');
+ },
+
+ setData: function() {
+ var args = [].slice.apply(arguments);
+ this.applyArgs(args, '_setData');
+ },
+
+ destroy: function() {
+ for(var i = 0; i < this.hooks.length; i++) {
+ this.hooks[i].destroy();
+ }
+ this.hooks = [];
+ this.removeEvents();
+ },
+
+ applyArgs: function(args, methodName) {
+ if(this.ready) {
+ this[methodName].apply(this, args);
+ } else {
+ this.queuedData = this.queuedData || [];
+ this.queuedData.push(args);
+ }
+ },
+
+ _addData: function(trigger, data) {
+ this._processData(trigger, data, 'addData');
+ },
+
+ _setData: function(trigger, data) {
+ this._processData(trigger, data, 'setData');
+ },
+
+ _processData: function(trigger, data, methodName) {
+ for(var i = 0; i < this.hooks.length; i++) {
+ var hook = this.hooks[i];
+ if(hook.trigger.dataset.hasOwnProperty('id')) {
+ if(hook.trigger.dataset.id === trigger) {
+ hook.list[methodName](data);
+ }
+ }
+ }
+ },
+
+ addEvents: function() {
+ var self = this;
+ this.windowClickedWrapper = function(e){
+ var thisTag = e.target;
+ if(thisTag.tagName !== 'UL'){
+ // climb up the tree to find the UL
+ thisTag = utils.closest(thisTag, 'UL');
+ }
+ if(utils.isDropDownParts(thisTag)){ return }
+ if(utils.isDropDownParts(e.target)){ return }
+ for(var i = 0; i < self.hooks.length; i++) {
+ self.hooks[i].list.hide();
+ }
+ }.bind(this);
+ document.addEventListener('click', this.windowClickedWrapper);
+ },
+
+ removeEvents: function(){
+ w.removeEventListener('click', this.windowClickedWrapper);
+ w.removeEventListener('load', this.loadWrapper);
+ },
+
+ changeHookList: function(trigger, list, plugins, config) {
+ trigger = document.querySelector('[data-id="'+trigger+'"]');
+ // list = document.querySelector(list);
+ this.hooks.every(function(hook, i) {
+ if(hook.trigger === trigger) {
+ hook.destroy();
+ this.hooks.splice(i, 1);
+ this.addHook(trigger, list, plugins, config);
+ return false;
+ }
+ return true
+ }.bind(this));
+ },
+
+ addHook: function(hook, list, plugins, config) {
+ if(!(hook instanceof HTMLElement) && typeof hook === 'string'){
+ hook = document.querySelector(hook);
+ }
+ 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));
+ } else if(hook.tagName === 'INPUT') {
+ this.hooks.push(new HookInput(hook, list, plugins, config));
+ }
+ }
+ return this;
+ },
+
+ addHooks: function(hooks, plugins, config) {
+ for(var i = 0; i < hooks.length; i++) {
+ var hook = hooks[i];
+ this.addHook(hook, null, plugins, config);
+ }
+ return this;
+ },
+
+ setConfig: function(obj){
+ this.config = obj;
+ },
+
+ init: function () {
+ this.addEvents();
+ var readyEvent = new CustomEvent('ready.dl', {
+ detail: {
+ dropdown: this,
+ },
+ });
+ window.dispatchEvent(readyEvent);
+ this.ready = true;
+ for(var i = 0; i < this.queuedData.length; i++) {
+ this.addData.apply(this, this.queuedData[i]);
+ }
+ this.queuedData = [];
+ return this;
+ },
+ });
+
+ return DropLab;
+ };
+});
+
+},{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){
+var DropDown = require('./dropdown');
+
+var Hook = function(trigger, list, plugins, config){
+ this.trigger = trigger;
+ this.list = new DropDown(list);
+ this.type = 'Hook';
+ this.event = 'click';
+ this.plugins = plugins || [];
+ this.config = config || {};
+ this.id = trigger.dataset.id;
+};
+
+Object.assign(Hook.prototype, {
+
+ addEvents: function(){},
+
+ constructor: Hook,
+});
+
+module.exports = Hook;
+
+},{"./dropdown":3}],6:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var Hook = require('./hook');
+
+var HookButton = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+ this.type = 'button';
+ this.event = 'click';
+ this.addEvents();
+ this.addPlugins();
+};
+
+HookButton.prototype = Object.create(Hook.prototype);
+
+Object.assign(HookButton.prototype, {
+ addPlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].init(this);
+ }
+ },
+
+ clicked: function(e){
+ var buttonEvent = new CustomEvent('click.dl', {
+ detail: {
+ hook: this,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ this.list.show();
+ e.target.dispatchEvent(buttonEvent);
+ },
+
+ addEvents: function(){
+ this.clickedWrapper = this.clicked.bind(this);
+ this.trigger.addEventListener('click', this.clickedWrapper);
+ },
+
+ removeEvents: function(){
+ this.trigger.removeEventListener('click', this.clickedWrapper);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].destroy();
+ }
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+ this.removeEvents();
+ this.removePlugins();
+ },
+
+
+ constructor: HookButton,
+});
+
+
+module.exports = HookButton;
+
+},{"./custom_event_polyfill":2,"./hook":5}],7:[function(require,module,exports){
+var CustomEvent = require('./custom_event_polyfill');
+var Hook = require('./hook');
+
+var HookInput = function(trigger, list, plugins, config) {
+ Hook.call(this, trigger, list, plugins, config);
+ this.type = 'input';
+ this.event = 'input';
+ this.addPlugins();
+ this.addEvents();
+};
+
+Object.assign(HookInput.prototype, {
+ addPlugins: function() {
+ var self = this;
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].init(self);
+ }
+ },
+
+ addEvents: function(){
+ var self = this;
+
+ this.mousedown = function mousedown(e) {
+ if(self.hasRemovedEvents) return;
+
+ var mouseEvent = new CustomEvent('mousedown.dl', {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(mouseEvent);
+ }
+
+ this.input = function input(e) {
+ if(self.hasRemovedEvents) return;
+
+ var inputEvent = new CustomEvent('input.dl', {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ },
+ bubbles: true,
+ 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){
+ var keyEvent = new CustomEvent(keyEventName, {
+ detail: {
+ hook: self,
+ text: e.target.value,
+ which: e.which,
+ key: e.key,
+ },
+ bubbles: true,
+ cancelable: true
+ });
+ e.target.dispatchEvent(keyEvent);
+ self.list.show();
+ }
+
+ this.events = this.events || {};
+ this.events.mousedown = this.mousedown;
+ this.events.input = this.input;
+ this.events.keyup = this.keyup;
+ this.events.keydown = this.keydown;
+ this.trigger.addEventListener('mousedown', this.mousedown);
+ this.trigger.addEventListener('input', this.input);
+ this.trigger.addEventListener('keyup', this.keyup);
+ this.trigger.addEventListener('keydown', this.keydown);
+ },
+
+ removeEvents: function() {
+ this.hasRemovedEvents = true;
+ this.trigger.removeEventListener('mousedown', this.mousedown);
+ this.trigger.removeEventListener('input', this.input);
+ this.trigger.removeEventListener('keyup', this.keyup);
+ this.trigger.removeEventListener('keydown', this.keydown);
+ },
+
+ restoreInitialState: function() {
+ this.list.list.innerHTML = this.list.initialState;
+ },
+
+ removePlugins: function() {
+ for(var i = 0; i < this.plugins.length; i++) {
+ this.plugins[i].destroy();
+ }
+ },
+
+ destroy: function() {
+ this.restoreInitialState();
+ this.removeEvents();
+ this.removePlugins();
+ this.list.destroy();
+ }
+});
+
+module.exports = HookInput;
+
+},{"./custom_event_polyfill":2,"./hook":5}],8:[function(require,module,exports){
+var DropLab = require('./droplab')();
+var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+var keyboard = require('./keyboard')();
+var setup = function() {
+ window.DropLab = DropLab;
+};
+
+
+module.exports = setup();
+
+},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){
+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');
+ for(var i = 0; i < listItems.length; i++) {
+ listItems[i].classList.remove('dropdown-active');
+ }
+ return listItems;
+ };
+
+ var setMenuForArrows = function setMenuForArrows(list) {
+ var listItems = removeHighlight(list);
+ if(currentIndex>0){
+ if(!listItems[currentIndex-1]){
+ currentIndex = currentIndex-1;
+ }
+ listItems[currentIndex-1].classList.add('dropdown-active');
+ }
+ };
+
+ var mousedown = function mousedown(e) {
+ var list = e.detail.hook.list;
+ removeHighlight(list);
+ list.show();
+ currentIndex = 0;
+ isUpArrow = false;
+ isDownArrow = false;
+ };
+ var selectItem = function selectItem(list) {
+ var listItems = removeHighlight(list);
+ var currentItem = listItems[currentIndex-1];
+ var listEvent = new CustomEvent('click.dl', {
+ detail: {
+ list: list,
+ selected: currentItem,
+ data: currentItem.dataset,
+ },
+ });
+ list.list.dispatchEvent(listEvent);
+ list.hide();
+ }
+
+ var keydown = function keydown(e){
+ var typedOn = e.target;
+ isUpArrow = false;
+ isDownArrow = false;
+
+ if(e.detail.which){
+ currentKey = e.detail.which;
+ if(currentKey === 13){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 38) {
+ isUpArrow = true;
+ }
+ if(currentKey === 40) {
+ isDownArrow = true;
+ }
+ } else if(e.detail.key) {
+ currentKey = e.detail.key;
+ if(currentKey === 'Enter'){
+ selectItem(e.detail.hook.list);
+ return;
+ }
+ if(currentKey === 'ArrowUp') {
+ isUpArrow = true;
+ }
+ if(currentKey === 'ArrowDown') {
+ isDownArrow = true;
+ }
+ }
+ if(isUpArrow){ currentIndex--; }
+ if(isDownArrow){ currentIndex++; }
+ if(currentIndex < 0){ currentIndex = 0; }
+ setMenuForArrows(e.detail.hook.list);
+ };
+
+ w.addEventListener('mousedown.dl', mousedown);
+ w.addEventListener('keydown.dl', keydown);
+ };
+});
+},{"./window":11}],10:[function(require,module,exports){
+var DATA_TRIGGER = require('./constants').DATA_TRIGGER;
+var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN;
+
+var toDataCamelCase = function(attr){
+ return this.camelize(attr.split('-').slice(1).join(' '));
+};
+
+// the tiniest damn templating I can do
+var t = function(s,d){
+ for(var p in d)
+ s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]);
+ return s;
+};
+
+var camelize = function(str) {
+ return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
+ return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
+ }).replace(/\s+/g, '');
+};
+
+var closest = function(thisTag, stopTag) {
+ while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){
+ thisTag = thisTag.parentNode;
+ }
+ return thisTag;
+};
+
+var isDropDownParts = function(target) {
+ if(!target || target.tagName === 'HTML') { return false; }
+ return (
+ target.hasAttribute(DATA_TRIGGER) ||
+ target.hasAttribute(DATA_DROPDOWN)
+ );
+};
+
+module.exports = {
+ toDataCamelCase: toDataCamelCase,
+ t: t,
+ camelize: camelize,
+ closest: closest,
+ isDropDownParts: isDropDownParts,
+};
+
+},{"./constants":1}],11:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[8])(8)
+});
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
new file mode 100644
index 00000000000..f20610b3811
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -0,0 +1,79 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ function droplabAjaxException(message) {
+ this.message = message;
+ }
+
+ w.droplabAjax = {
+ _loadUrlData: function _loadUrlData(url) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+
+ init: function init(hook) {
+ var self = this;
+ var config = hook.config.droplabAjax;
+
+ if (!config || !config.endpoint || !config.method) {
+ return;
+ }
+
+ if (config.method !== 'setData' && config.method !== 'addData') {
+ return;
+ }
+
+ if (config.loadingTemplate) {
+ var dynamicList = hook.list.list.querySelector('[data-dynamic]');
+
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', '');
+
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+
+ this._loadUrlData(config.endpoint)
+ .then(function(d) {
+ if (config.loadingTemplate) {
+ var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
+
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+ }
+ hook.list[config.method].call(hook.list, d);
+ }).catch(function(e) {
+ throw new droplabAjaxException(e.message || e);
+ });
+ },
+
+ destroy: function() {
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[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
new file mode 100644
index 00000000000..af163f76851
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -0,0 +1,145 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ w.droplabAjaxFilter = {
+ init: function(hook) {
+ this.destroyed = false;
+ this.hook = hook;
+ this.notLoading();
+
+ this.debounceTriggerWrapper = this.debounceTrigger.bind(this);
+ this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper);
+ this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper);
+ this.trigger(true);
+ },
+
+ notLoading: function notLoading() {
+ this.loading = false;
+ },
+
+ debounceTrigger: function debounceTrigger(e) {
+ var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
+ var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
+ var focusEvent = e.type === 'focus';
+
+ if (invalidKeyPressed || this.loading) {
+ return;
+ }
+
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
+ },
+
+ trigger: function trigger(getEntireList) {
+ var config = this.hook.config.droplabAjaxFilter;
+ var searchValue = this.trigger.value;
+
+ if (!config || !config.endpoint || !config.searchKey) {
+ return;
+ }
+
+ if (config.searchValueFunction) {
+ searchValue = config.searchValueFunction();
+ }
+
+ if (config.loadingTemplate && this.hook.list.data === undefined ||
+ this.hook.list.data.length === 0) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+
+ var loadingTemplate = document.createElement('div');
+ loadingTemplate.innerHTML = config.loadingTemplate;
+ loadingTemplate.setAttribute('data-loading-template', true);
+
+ this.listTemplate = dynamicList.outerHTML;
+ dynamicList.outerHTML = loadingTemplate.outerHTML;
+ }
+
+ if (getEntireList) {
+ searchValue = '';
+ }
+
+ if (config.searchKey === searchValue) {
+ return this.list.show();
+ }
+
+ this.loading = true;
+
+ var params = config.params || {};
+ params[config.searchKey] = searchValue;
+ var self = this;
+ this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) {
+ if (config.loadingTemplate && self.hook.list.data === undefined ||
+ self.hook.list.data.length === 0) {
+ const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
+
+ if (dataLoadingTemplate) {
+ dataLoadingTemplate.outerHTML = self.listTemplate;
+ }
+ }
+
+ if (!self.destroyed) {
+ var hookListChildren = self.hook.list.list.children;
+ var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
+
+ if (onlyDynamicList && data.length === 0) {
+ self.hook.list.hide();
+ }
+
+ self.hook.list.setData.call(self.hook.list, data);
+ }
+ self.notLoading();
+ });
+ },
+
+ _loadUrlData: function _loadUrlData(url) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest;
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function () {
+ if(xhr.readyState === XMLHttpRequest.DONE) {
+ if (xhr.status === 200) {
+ var data = JSON.parse(xhr.responseText);
+ return resolve(data);
+ } else {
+ return reject([xhr.responseText, xhr.status]);
+ }
+ }
+ };
+ xhr.send();
+ });
+ },
+
+ buildParams: function(params) {
+ if (!params) return '';
+ var paramsArray = Object.keys(params).map(function(param) {
+ return param + '=' + (params[param] || '');
+ });
+ return '?' + paramsArray.join('&');
+ },
+
+ destroy: function destroy() {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ this.destroyed = true;
+
+ this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper);
+ this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper);
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[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
new file mode 100644
index 00000000000..41a220831f9
--- /dev/null
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -0,0 +1,60 @@
+/* eslint-disable */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* global droplab */
+
+require('../window')(function(w){
+ w.droplabFilter = {
+
+ keydownWrapper: function(e){
+ var list = e.detail.hook.list;
+ var data = list.data;
+ var value = e.detail.hook.trigger.value.toLowerCase();
+ var config = e.detail.hook.config.droplabFilter;
+ var matches = [];
+ var filterFunction;
+ // will only work on dynamically set data
+ if(!data){
+ return;
+ }
+
+ if (config && config.filterFunction && typeof config.filterFunction === 'function') {
+ filterFunction = config.filterFunction;
+ } else {
+ filterFunction = function(o){
+ // cheap string search
+ o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
+ return o;
+ };
+ }
+
+ matches = data.map(function(o) {
+ return filterFunction(o, value);
+ });
+ list.render(matches);
+ },
+
+ init: function init(hookInput) {
+ var config = hookInput.config.droplabFilter;
+
+ if (!config || (!config.template && !config.filterFunction)) {
+ return;
+ }
+
+ this.hookInput = hookInput;
+ this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
+ },
+
+ destroy: function destroy(){
+ this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
+ }
+ };
+});
+},{"../window":2}],2:[function(require,module,exports){
+module.exports = function(callback) {
+ return (function() {
+ callback(this);
+ }).call(null);
+};
+
+},{}]},{},[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/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
new file mode 100644
index 00000000000..f4ec3b206cc
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
@@ -0,0 +1,66 @@
+/*= require filtered_search/filtered_search_dropdown */
+
+/* global droplabFilter */
+
+(() => {
+ class DropdownHint extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ droplabFilter: {
+ template: 'hint',
+ filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
+ },
+ };
+ }
+
+ itemClicked(e) {
+ const { selected } = e.detail;
+
+ if (selected.tagName === 'LI') {
+ if (selected.hasAttribute('data-value')) {
+ this.dismissDropdown();
+ } else {
+ const token = selected.querySelector('.js-filter-hint').innerText.trim();
+ const tag = selected.querySelector('.js-filter-tag').innerText.trim();
+
+ if (tag.length) {
+ gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
+ }
+ this.dismissDropdown();
+ this.dispatchInputEvent();
+ }
+ }
+ }
+
+ renderContent() {
+ const dropdownData = [{
+ icon: 'fa-pencil',
+ hint: 'author:',
+ tag: '&lt;@author&gt;',
+ }, {
+ icon: 'fa-user',
+ hint: 'assignee:',
+ tag: '&lt;@assignee&gt;',
+ }, {
+ icon: 'fa-clock-o',
+ hint: 'milestone:',
+ tag: '&lt;%milestone&gt;',
+ }, {
+ icon: 'fa-tag',
+ hint: 'label:',
+ tag: '&lt;~label&gt;',
+ }];
+
+ this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
+ this.droplab.setData(this.hookId, dropdownData);
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownHint = DropdownHint;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
new file mode 100644
index 00000000000..13cbec1be4a
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
@@ -0,0 +1,44 @@
+/*= require filtered_search/filtered_search_dropdown */
+
+/* global droplabAjax */
+/* global droplabFilter */
+
+(() => {
+ class DropdownNonUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter, endpoint, symbol) {
+ super(droplab, dropdown, input, filter);
+ this.symbol = symbol;
+ this.config = {
+ droplabAjax: {
+ endpoint,
+ method: 'setData',
+ loadingTemplate: this.loadingTemplate,
+ },
+ droplabFilter: {
+ filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
+ },
+ };
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e, (selected) => {
+ const title = selected.querySelector('.js-data-value').innerText.trim();
+ return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
+ });
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab
+ .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ init() {
+ this.droplab
+ .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownNonUser = DropdownNonUser;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
new file mode 100644
index 00000000000..7bf199d9274
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -0,0 +1,53 @@
+/*= require filtered_search/filtered_search_dropdown */
+
+/* global droplabAjaxFilter */
+
+(() => {
+ class DropdownUser extends gl.FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ super(droplab, dropdown, input, filter);
+ this.config = {
+ droplabAjaxFilter: {
+ endpoint: '/autocomplete/users.json',
+ searchKey: 'search',
+ params: {
+ per_page: 20,
+ active: true,
+ project_id: this.getProjectId(),
+ current_user: true,
+ },
+ searchValueFunction: this.getSearchInput.bind(this),
+ loadingTemplate: this.loadingTemplate,
+ },
+ };
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e,
+ selected => selected.querySelector('.dropdown-light-content').innerText.trim());
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ getProjectId() {
+ return this.input.getAttribute('data-project-id');
+ }
+
+ getSearchInput() {
+ const query = gl.DropdownUtils.getSearchInput(this.input);
+ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+
+ return lastToken.value || '';
+ }
+
+ init() {
+ this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.DropdownUser = DropdownUser;
+})();
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
new file mode 100644
index 00000000000..443ac222f70
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -0,0 +1,120 @@
+(() => {
+ class DropdownUtils {
+ static getEscapedText(text) {
+ let escapedText = text;
+ const hasSpace = text.indexOf(' ') !== -1;
+ const hasDoubleQuote = text.indexOf('"') !== -1;
+
+ // Encapsulate value with quotes if it has spaces
+ // Known side effect: values's with both single and double quotes
+ // won't escape properly
+ if (hasSpace) {
+ if (hasDoubleQuote) {
+ escapedText = `'${text}'`;
+ } else {
+ // Encapsulate singleQuotes or if it hasSpace
+ escapedText = `"${text}"`;
+ }
+ }
+
+ return escapedText;
+ }
+
+ 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();
+ value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -1));
+
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
+
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+ } else {
+ updatedItem.droplab_hidden = false;
+ }
+
+ return updatedItem;
+ }
+
+ static filterHint(input, item) {
+ const updatedItem = item;
+ const query = gl.DropdownUtils.getSearchInput(input);
+ let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ lastToken = lastToken.key || lastToken || '';
+
+ if (!lastToken || query.split('').last() === ' ') {
+ updatedItem.droplab_hidden = false;
+ } else if (lastToken) {
+ const split = lastToken.split(':');
+ const tokenName = split[0].split(' ').last();
+
+ const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
+ updatedItem.droplab_hidden = tokenName ? match : false;
+ }
+
+ return updatedItem;
+ }
+
+ static setDataValueIfSelected(filter, selected) {
+ const dataValue = selected.getAttribute('data-value');
+
+ if (dataValue) {
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
+ }
+
+ // 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(/"(.*?)"/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 || {};
+ gl.DropdownUtils = DropdownUtils;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
new file mode 100644
index 00000000000..d188718c5f3
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -0,0 +1,7 @@
+ // This is a manifest file that'll be compiled into including all the files listed below.
+ // Add new JavaScript code in separate files in this directory and they'll automatically
+ // be included in the compiled file accessible from http://example.com/assets/application.js
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+ // the compiled file.
+ //
+ /*= require_tree . */
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
new file mode 100644
index 00000000000..9128ea907b3
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -0,0 +1,105 @@
+(() => {
+ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
+
+ class FilteredSearchDropdown {
+ constructor(droplab, dropdown, input, filter) {
+ this.droplab = droplab;
+ this.hookId = input.getAttribute('data-id');
+ this.input = input;
+ this.filter = filter;
+ this.dropdown = dropdown;
+ this.loadingTemplate = `<div class="filter-dropdown-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>`;
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ this.itemClickedWrapper = this.itemClicked.bind(this);
+ this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
+ }
+
+ unbindEvents() {
+ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
+ }
+
+ getCurrentHook() {
+ return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
+ }
+
+ itemClicked(e, getValueFunction) {
+ const { selected } = e.detail;
+
+ if (selected.tagName === 'LI' && selected.innerHTML) {
+ const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
+
+ if (!dataValueSet) {
+ const value = getValueFunction(selected);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
+ }
+
+ this.dismissDropdown();
+ }
+ }
+
+ setAsDropdown() {
+ this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
+ }
+
+ setOffset(offset = 0) {
+ this.dropdown.style.left = `${offset}px`;
+ }
+
+ renderContent(forceShowList = false) {
+ if (forceShowList && this.getCurrentHook().list.hidden) {
+ this.getCurrentHook().list.show();
+ }
+ }
+
+ render(forceRenderContent = false, forceShowList = false) {
+ this.setAsDropdown();
+
+ const currentHook = this.getCurrentHook();
+ const firstTimeInitialized = currentHook === null;
+
+ if (firstTimeInitialized || forceRenderContent) {
+ this.renderContent(forceShowList);
+ } else if (currentHook.list.list.id !== this.dropdown.id) {
+ this.renderContent(forceShowList);
+ }
+ }
+
+ dismissDropdown() {
+ // Focusing on the input will dismiss dropdown
+ // (default droplab functionality)
+ this.input.focus();
+ }
+
+ dispatchInputEvent() {
+ // Propogate input change to FilteredSearchDropdownManager
+ // so that it can determine which dropdowns to open
+ this.input.dispatchEvent(new CustomEvent('input', {
+ bubbles: true,
+ cancelable: true,
+ }));
+ }
+
+ hideDropdown() {
+ this.getCurrentHook().list.hide();
+ }
+
+ resetFilters() {
+ const hook = this.getCurrentHook();
+ const data = hook.list.data;
+ const results = data.map((o) => {
+ const updated = o;
+ updated.droplab_hidden = false;
+ return updated;
+ });
+ hook.list.render(results);
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchDropdown = FilteredSearchDropdown;
+})();
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
new file mode 100644
index 00000000000..408a0dfd768
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -0,0 +1,199 @@
+/* global DropLab */
+
+(() => {
+ class FilteredSearchDropdownManager {
+ constructor() {
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.filteredSearchInput = document.querySelector('.filtered-search');
+
+ this.setupMapping();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('page:fetch', this.cleanupWrapper);
+ }
+
+ cleanup() {
+ if (this.droplab) {
+ this.droplab.destroy();
+ this.droplab = null;
+ }
+
+ this.setupMapping();
+
+ document.removeEventListener('page:fetch', this.cleanupWrapper);
+ }
+
+ setupMapping() {
+ this.mapping = {
+ author: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: document.querySelector('#js-dropdown-author'),
+ },
+ assignee: {
+ reference: null,
+ gl: 'DropdownUser',
+ element: document.querySelector('#js-dropdown-assignee'),
+ },
+ milestone: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: ['milestones.json', '%'],
+ element: document.querySelector('#js-dropdown-milestone'),
+ },
+ label: {
+ reference: null,
+ gl: 'DropdownNonUser',
+ extraArguments: ['labels.json', '~'],
+ element: document.querySelector('#js-dropdown-label'),
+ },
+ hint: {
+ reference: null,
+ gl: 'DropdownHint',
+ element: document.querySelector('#js-dropdown-hint'),
+ },
+ };
+ }
+
+ static addWordToInput(tokenName, tokenValue = '') {
+ const input = document.querySelector('.filtered-search');
+ const inputValue = input.value;
+ const word = `${tokenName}:${tokenValue}`;
+
+ // Get the string to replace
+ const selectionStart = input.selectionStart;
+ const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
+
+ input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
+ gl.FilteredSearchDropdownManager.updateInputCaretPosition(selectionStart, 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() {
+ this.updateDropdownOffset(this.currentDropdown);
+ }
+
+ updateDropdownOffset(key) {
+ if (!this.font) {
+ this.font = window.getComputedStyle(this.filteredSearchInput).font;
+ }
+
+ const input = this.filteredSearchInput;
+ const inputText = input.value.slice(0, input.selectionStart);
+ const filterIconPadding = 27;
+ 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);
+ }
+
+ load(key, firstLoad = false) {
+ const mappingKey = this.mapping[key];
+ const glClass = mappingKey.gl;
+ const element = mappingKey.element;
+ let forceShowList = false;
+
+ if (!mappingKey.reference) {
+ const dl = this.droplab;
+ const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
+ const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
+
+ // Passing glArguments to `new gl[glClass](<arguments>)`
+ mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
+ }
+
+ if (firstLoad) {
+ mappingKey.reference.init();
+ }
+
+ if (this.currentDropdown === 'hint') {
+ // Force the dropdown to show if it was clicked from the hint dropdown
+ forceShowList = true;
+ }
+
+ this.updateDropdownOffset(key);
+ mappingKey.reference.render(firstLoad, forceShowList);
+
+ this.currentDropdown = key;
+ }
+
+ loadDropdown(dropdownName = '') {
+ let firstLoad = false;
+
+ if (!this.droplab) {
+ firstLoad = true;
+ this.droplab = new DropLab();
+ }
+
+ const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
+ const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
+ && this.mapping[match.key];
+ const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
+
+ if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
+ const key = match && match.key ? match.key : 'hint';
+ this.load(key, firstLoad);
+ }
+ }
+
+ setDropdown() {
+ const { lastToken, searchToken } = this.tokenizer
+ .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
+
+ if (this.currentDropdown) {
+ this.updateCurrentDropdownOffset();
+ }
+
+ if (lastToken === searchToken && lastToken !== null) {
+ // Token is not fully initialized yet because it has no value
+ // Eg. token = 'label:'
+
+ const split = lastToken.split(':');
+ const dropdownName = split[0].split(' ').last();
+ this.loadDropdown(split.length > 1 ? dropdownName : '');
+ } else if (lastToken) {
+ // Token has been initialized into an object because it has a value
+ this.loadDropdown(lastToken.key);
+ } else {
+ this.loadDropdown('hint');
+ }
+ }
+
+ resetDropdowns() {
+ // Force current dropdown to hide
+ this.mapping[this.currentDropdown].reference.hideDropdown();
+
+ // Re-Load dropdown
+ this.setDropdown();
+
+ // Reset filters for current dropdown
+ this.mapping[this.currentDropdown].reference.resetFilters();
+
+ // Reposition dropdown so that it is aligned with cursor
+ this.updateDropdownOffset(this.currentDropdown);
+ }
+
+ destroyDroplab() {
+ this.droplab.destroy();
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
new file mode 100644
index 00000000000..c7b72b36561
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -0,0 +1,208 @@
+/* global Turbolinks */
+
+(() => {
+ class FilteredSearchManager {
+ constructor() {
+ this.filteredSearchInput = document.querySelector('.filtered-search');
+ this.clearSearchButton = document.querySelector('.clear-search');
+
+ if (this.filteredSearchInput) {
+ this.tokenizer = gl.FilteredSearchTokenizer;
+ this.dropdownManager = new gl.FilteredSearchDropdownManager();
+
+ this.bindEvents();
+ this.loadSearchParamsFromURL();
+ this.dropdownManager.setDropdown();
+
+ this.cleanupWrapper = this.cleanup.bind(this);
+ document.addEventListener('page:fetch', this.cleanupWrapper);
+ }
+ }
+
+ cleanup() {
+ this.unbindEvents();
+ document.removeEventListener('page:fetch', this.cleanupWrapper);
+ }
+
+ bindEvents() {
+ 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.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.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);
+ }
+
+ checkForBackspace(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ // Reposition dropdown so that it is aligned with cursor
+ this.dropdownManager.updateCurrentDropdownOffset();
+ }
+ }
+
+ checkForEnter(e) {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
+
+ this.search();
+ }
+ }
+
+ toggleClearSearchButton(e) {
+ if (e.target.value) {
+ this.clearSearchButton.classList.remove('hidden');
+ } else {
+ this.clearSearchButton.classList.add('hidden');
+ }
+ }
+
+ clearSearch(e) {
+ e.preventDefault();
+
+ this.filteredSearchInput.value = '';
+ this.clearSearchButton.classList.add('hidden');
+
+ this.dropdownManager.resetDropdowns();
+ }
+
+ loadSearchParamsFromURL() {
+ const params = gl.utils.getUrlParamsArray();
+ const usernameParams = this.getUsernameParams();
+ const inputValues = [];
+
+ params.forEach((p) => {
+ const split = p.split('=');
+ const keyParam = decodeURIComponent(split[0]);
+ const value = split[1];
+
+ // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p);
+
+ if (condition) {
+ inputValues.push(`${condition.tokenKey}:${condition.value}`);
+ } else {
+ // Sanitize value since URL converts spaces into +
+ // Replace before decode so that we know what was originally + versus the encoded +
+ const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
+ const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam);
+
+ if (match) {
+ const indexOf = keyParam.indexOf('_');
+ const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
+ const symbol = match.symbol;
+ let quotationsToUse = '';
+
+ if (sanitizedValue.indexOf(' ') !== -1) {
+ // Prefer ", but use ' if required
+ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
+ }
+
+ 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);
+ }
+ }
+ });
+
+ // Trim the last space value
+ this.filteredSearchInput.value = inputValues.join(' ');
+
+ if (inputValues.length > 0) {
+ this.clearSearchButton.classList.remove('hidden');
+ }
+ }
+
+ search() {
+ const paths = [];
+ const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
+ const currentState = gl.utils.getParameterByName('state') || 'opened';
+ paths.push(`state=${currentState}`);
+
+ tokens.forEach((token) => {
+ const condition = gl.FilteredSearchTokenKeys
+ .searchByConditionKeyValue(token.key, token.value.toLowerCase());
+ const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key);
+ const keyParam = param ? `${token.key}_${param}` : token.key;
+ let tokenPath = '';
+
+ if (condition) {
+ tokenPath = condition.url;
+ } else {
+ let tokenValue = token.value;
+
+ if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
+ (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
+ tokenValue = tokenValue.slice(1, tokenValue.length - 1);
+ }
+
+ tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
+ }
+
+ paths.push(tokenPath);
+ });
+
+ if (searchToken) {
+ paths.push(`search=${encodeURIComponent(searchToken)}`);
+ }
+
+ 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 || {};
+ gl.FilteredSearchManager = FilteredSearchManager;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
new file mode 100644
index 00000000000..e46373024b6
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6
@@ -0,0 +1,83 @@
+(() => {
+ const tokenKeys = [{
+ key: 'author',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ }, {
+ key: 'assignee',
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ }, {
+ key: 'milestone',
+ type: 'string',
+ param: 'title',
+ symbol: '%',
+ }, {
+ key: 'label',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+ }];
+
+ const conditions = [{
+ url: 'assignee_id=0',
+ tokenKey: 'assignee',
+ value: 'none',
+ }, {
+ url: 'milestone_title=No+Milestone',
+ tokenKey: 'milestone',
+ value: 'none',
+ }, {
+ url: 'milestone_title=%23upcoming',
+ tokenKey: 'milestone',
+ value: 'upcoming',
+ }, {
+ url: 'label_name[]=No+Label',
+ tokenKey: 'label',
+ value: 'none',
+ }];
+
+ class FilteredSearchTokenKeys {
+ static get() {
+ return tokenKeys;
+ }
+
+ static getConditions() {
+ return conditions;
+ }
+
+ static searchByKey(key) {
+ return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
+ }
+
+ static searchBySymbol(symbol) {
+ return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
+ }
+
+ static searchByKeyParam(keyParam) {
+ return tokenKeys.find((tokenKey) => {
+ let tokenKeyParam = tokenKey.key;
+
+ if (tokenKey.param) {
+ tokenKeyParam += `_${tokenKey.param}`;
+ }
+
+ return keyParam === tokenKeyParam;
+ }) || null;
+ }
+
+ static searchByConditionUrl(url) {
+ return conditions.find(condition => condition.url === url) || null;
+ }
+
+ static searchByConditionKeyValue(key, value) {
+ return conditions
+ .find(condition => condition.tokenKey === key && condition.value === value) || null;
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
+})();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
new file mode 100644
index 00000000000..cf53845a48b
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6
@@ -0,0 +1,45 @@
+(() => {
+ class FilteredSearchTokenizer {
+ static processTokens(input) {
+ // Regex extracts `(token):(symbol)(value)`
+ // Values that start with a double quote must end in a double quote (same for single)
+ const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g;
+ const tokens = [];
+ let lastToken = null;
+ const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
+ let tokenValue = v1 || v2 || v3;
+ let tokenSymbol = symbol;
+
+ if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
+ tokenSymbol = tokenValue;
+ tokenValue = '';
+ }
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ return '';
+ }).replace(/\s{2,}/g, ' ').trim() || '';
+
+ if (tokens.length > 0) {
+ const last = tokens[tokens.length - 1];
+ const lastString = `${last.key}:${last.symbol}${last.value}`;
+ lastToken = input.lastIndexOf(lastString) ===
+ input.length - lastString.length ? last : searchToken;
+ } else {
+ lastToken = searchToken;
+ }
+
+ return {
+ tokens,
+ lastToken,
+ searchToken,
+ };
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
+})();
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 bb516b3d2df..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;
@@ -476,7 +474,7 @@
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
- $input.blur().val("");
+ $input.blur();
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
@@ -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..c260ad03d47 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_';
@@ -163,5 +162,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 b8d637a9827..6d57d31f380 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;
@@ -124,6 +124,12 @@
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
};
+ gl.utils.getUrlParamsArray = function () {
+ // We can trust that each param has one & since values containing & will be encoded
+ // Remove the first character of search as it is always ?
+ return window.location.search.slice(1).split('&');
+ };
+
gl.utils.isMetaKey = function(e) {
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
@@ -153,7 +159,5 @@
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
-
})(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 5066e3282d7..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,13 +11,28 @@
}
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);
};
gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end);
};
+ gl.text.getTextWidth = function(text, font) {
+ /**
+ * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+ *
+ * @param {String} text The text to be rendered.
+ * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+ *
+ * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+ */
+ // re-use canvas object for better performance
+ var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
+ var context = canvas.getContext('2d');
+ context.font = font;
+ return context.measureText(text).width;
+ };
gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
};
@@ -144,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_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
index 860e7e066a0..4c8c28af755 100644
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -237,13 +237,8 @@
}
this.diffsLoaded = true;
- const diffPage = new gl.Diff();
-
- const locationHash = gl.utils.getLocationHash();
- const anchoredDiff = locationHash && locationHash.split('_')[0];
- if (anchoredDiff) {
- diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs'));
- }
+ new gl.Diff();
+ this.scrollToElement('#diffs');
},
});
}
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 603db88567d..06a72efa21d 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,8 +1,9 @@
-/* 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 */
/* global ResolveService */
+/* global mrRefreshWidgetUrl */
/*= require autosave */
/*= require autosize */
@@ -13,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;
@@ -244,6 +245,16 @@
};
+ Notes.prototype.handleCreateChanges = function(note) {
+ if (typeof note === 'undefined') {
+ return;
+ }
+
+ if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) {
+ $.get(mrRefreshWidgetUrl);
+ }
+ };
+
/*
Render note in main comments area.
@@ -429,6 +440,7 @@
*/
Notes.prototype.addNote = function(xhr, note, status) {
+ this.handleCreateChanges(note);
return this.renderNote(note);
};
@@ -508,7 +520,7 @@
}
return isAllowed;
- }
+ };
/*
@@ -903,7 +915,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);
@@ -917,7 +929,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() {
@@ -962,7 +974,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 4b6ebadeac7..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() {
@@ -12,6 +12,9 @@
selectable: true,
filterable: true,
fieldName: 'group_id',
+ search: {
+ fields: ['name']
+ },
data: function(term, callback) {
return Api.groups(term, {}, function(data) {
data.unshift({
@@ -40,6 +43,9 @@
selectable: true,
filterable: true,
fieldName: 'project_id',
+ search: {
+ fields: ['name']
+ },
data: function(term, callback) {
return Api.projects(term, 'id', function(data) {
data.unshift({
@@ -90,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 437f5dbbf7d..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) {
@@ -142,8 +141,9 @@
}
getCategoryContents() {
- var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
+ var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
userId = gon.current_user_id;
+ userName = gon.current_username;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
if (utils.isInGroupsPage() && groupOptions) {
options = groupOptions[utils.getGroupSlug()];
@@ -158,10 +158,10 @@
header: "" + name
}, {
text: 'Issues assigned to me',
- url: issuesPath + "/?assignee_id=" + userId
+ url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
- url: issuesPath + "/?author_id=" + userId
+ url: issuesPath + "/?author_username=" + userName
}, 'separator', {
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId
@@ -215,7 +215,7 @@
this.dropdown.addClass('open').trigger('shown.bs.dropdown');
return this.searchInput.removeClass('disabled');
}
- };
+ }
// Saves last length of the entered text
onSearchInputKeyDown() {
@@ -279,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
@@ -304,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]);
}
@@ -330,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') {
@@ -360,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) {
@@ -383,8 +383,7 @@
this.disableAutocomplete();
return this.searchInput.val('').focus();
}
- };
-
+ }
}
global.SearchAutocomplete = SearchAutocomplete;
@@ -425,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 ac8603ccd10..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, 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;
@@ -14,8 +14,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
- function SingleFileDiff(file, forceLoad, cb) {
- var clickTarget;
+ function SingleFileDiff(file) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
@@ -33,14 +32,13 @@
this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down');
}
- clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
- if (forceLoad) {
- this.toggleDiff({ target: clickTarget }, cb);
- }
+
+ $('.file-title, .click-to-expand', this.file).on('click', (function (e) {
+ this.toggleDiff($(e.target));
+ }).bind(this));
}
- SingleFileDiff.prototype.toggleDiff = function(e, cb) {
- var $target = $(e.target);
+ SingleFileDiff.prototype.toggleDiff = function($target, cb) {
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
@@ -88,15 +86,13 @@
};
return SingleFileDiff;
-
})();
- $.fn.singleFileDiff = function(forceLoad, cb) {
+ $.fn.singleFileDiff = function() {
return this.each(function() {
- if (!$.data(this, 'singleFileDiff') || forceLoad) {
- return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb));
+ if (!$.data(this, 'singleFileDiff')) {
+ return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
}
});
};
-
}).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/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/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
index 9dfbedd73ab..edd01f17a97 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6
@@ -1,5 +1,6 @@
/* global Vue, VueResource, gl */
/*= require vue_common_component/commit */
+/*= require vue_pagination/index */
/*= require vue-resource
/*= require boards/vue_resource_interceptor */
/*= require ./status.js.es6 */
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
index 73627e9ba50..b2ed05503c9 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -73,12 +73,12 @@
<table class="table ci-table">
<thead>
<tr>
- <th>Status</th>
- <th>Pipeline</th>
- <th>Commit</th>
- <th>Stages</th>
- <th></th>
- <th class="hidden-xs"></th>
+ <th class="pipeline-status">Status</th>
+ <th class="pipeline-info">Pipeline</th>
+ <th class="pipeline-commit">Commit</th>
+ <th class="pipeline-stages">Stages</th>
+ <th class="pipeline-date"></th>
+ <th class="pipeline-actions hidden-xs"></th>
</tr>
</thead>
<tbody>
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
index 74a79dcedae..4e85f16ebc5 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -5,37 +5,57 @@
gl.VueStage = Vue.extend({
data() {
return {
- request: false,
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.request) return this.clearBuilds();
+ 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.request = true;
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
- this.request = false;
return flash;
});
},
- clearBuilds() {
- this.builds = '';
- this.request = false;
+ 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() {
- return this.request ? this.builds : this.spinner;
+ return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
- if (this.request) return 'js-builds-dropdown-container';
+ if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
@@ -45,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)];
},
@@ -56,19 +76,25 @@
template: `
<div>
<button
- @click='fetchBuilds'
- @blur='fetchBuilds'
+ @click='fetchBuilds($event)'
:class="triggerButtonClass"
:title='stage.title'
data-placement="top"
data-toggle="dropdown"
- type="button">
+ type="button"
+ >
<span v-html="svg"></span>
<i class="fa fa-caret-down "></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
+ @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 6b34839b030..1982142853a 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -3,14 +3,24 @@
/*= require vue_realtime_listener/index.js */
((gl) => {
- const pageValues = headers => ({
- perPage: +headers['X-Per-Page'],
- page: +headers['X-Page'],
- total: +headers['X-Total'],
- totalPages: +headers['X-Total-Pages'],
- nextPage: +headers['X-Next-Page'],
- previousPage: +headers['X-Prev-Page'],
- });
+ const pageValues = (headers) => {
+ const normalizedHeaders = {};
+
+ Object.keys(headers).forEach((e) => {
+ normalizedHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ 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'],
+ };
+
+ return paginationInfo;
+ };
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
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/animations.scss b/app/assets/stylesheets/framework/animations.scss
index f1d36efb3de..8d38fc78a19 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -50,3 +50,77 @@
.pulse {
@include webkit-prefix(animation-name, pulse);
}
+
+/*
+* General hover animations
+*/
+
+
+// Sass multiple transitions mixin | https://gist.github.com/tobiasahlin/7a421fb9306a4f518aab
+// Usage: @include transition(width, height 0.3s ease-in-out);
+// Output: -webkit-transition(width 0.2s, height 0.3s ease-in-out);
+// transition(width 0.2s, height 0.3s ease-in-out);
+//
+// Pass in any number of transitions
+@mixin transition($transitions...) {
+ $unfoldedTransitions: ();
+ @each $transition in $transitions {
+ $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma);
+ }
+
+ transition: $unfoldedTransitions;
+}
+
+@function unfoldTransition ($transition) {
+ // Default values
+ $property: all;
+ $duration: $general-hover-transition-duration;
+ $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want
+ $delay: null; // Browser default is 0, which is what we want
+ $defaultProperties: ($property, $duration, $easing, $delay);
+
+ // Grab transition properties if they exist
+ $unfoldedTransition: ();
+ @for $i from 1 through length($defaultProperties) {
+ $p: null;
+ @if $i <= length($transition) {
+ $p: nth($transition, $i);
+ } @else {
+ $p: nth($defaultProperties, $i);
+ }
+ $unfoldedTransition: append($unfoldedTransition, $p);
+ }
+
+ @return $unfoldedTransition;
+}
+
+.btn,
+.side-nav-toggle {
+ @include transition(background-color, border-color, color, box-shadow);
+}
+
+.dropdown-menu-toggle,
+.avatar-circle,
+.header-user-avatar {
+ @include transition(border-color);
+}
+
+.note-action-button .link-highlight,
+.toolbar-btn,
+.dropdown-toggle-caret,
+.fa:not(.fa-bell) {
+ @include transition(color);
+}
+
+a {
+ @include transition(background-color, color, border);
+}
+
+.tree-table td,
+.well-list > li {
+ @include transition(background-color, border-color);
+}
+
+.stage-nav-item {
+ @include transition(background-color, box-shadow);
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 48827578d94..8392b98f0a7 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -52,6 +52,10 @@
border-radius: 0;
border: none;
}
+
+ &:not([href]):hover {
+ border-color: rgba($avatar-border, .2);
+ }
}
.identicon {
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 19827943385..d957ec64654 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -23,3 +23,118 @@
}
}
+.filtered-search-container {
+ display: -webkit-flex;
+ display: flex;
+}
+
+.filtered-search-input-container {
+ display: -webkit-flex;
+ display: flex;
+ position: relative;
+ width: 100%;
+
+ .form-control {
+ padding-left: 25px;
+ padding-right: 25px;
+
+ &:focus ~ .fa-filter {
+ color: $common-gray-dark;
+ }
+ }
+
+ .fa-filter {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ color: $gray-darkest;
+ }
+
+ .fa-times {
+ right: 10px;
+ color: $gray-darkest;
+ }
+
+ .clear-search {
+ width: 35px;
+ background-color: transparent;
+ border: none;
+ position: absolute;
+ right: 0;
+ height: 100%;
+ outline: none;
+
+ &:hover .fa-times {
+ color: $common-gray-dark;
+ }
+ }
+}
+
+.dropdown-menu .filter-dropdown-item {
+ padding: 0;
+}
+
+.filter-dropdown {
+ max-height: 215px;
+ overflow: auto;
+}
+
+.filter-dropdown-item {
+ .btn {
+ border: none;
+ width: 100%;
+ text-align: left;
+ padding: 8px 16px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ border-radius: 0;
+
+ .fa {
+ width: 15px;
+ }
+
+ .dropdown-label-box {
+ border-color: $white-light;
+ border-style: solid;
+ border-width: 1px;
+ width: 17px;
+ height: 17px;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ text-decoration: none;
+
+ .avatar {
+ border-color: $white-light;
+ }
+ }
+ }
+
+ .dropdown-light-content {
+ font-size: 14px;
+ font-weight: 400;
+ }
+
+ .dropdown-user {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .dropdown-user-details {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ }
+}
+
+.hint-dropdown {
+ width: 250px;
+}
+
+.filter-dropdown-loading {
+ padding: 8px 16px;
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 72b3fe2016c..24a1ce2b84d 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -57,6 +57,14 @@ header {
&.header-user-dropdown-toggle {
margin-left: 14px;
+
+ &:hover,
+ &:focus,
+ &:active {
+ .header-user-avatar {
+ border-color: rgba($avatar-border, .2);
+ }
+ }
}
&:hover,
@@ -104,6 +112,7 @@ header {
&:hover {
background-color: $white-normal;
+ color: $gl-header-nav-hover-color;
}
}
}
@@ -180,6 +189,7 @@ header {
&:hover {
text-decoration: underline;
+ color: $gl-header-nav-hover-color;
}
}
@@ -198,7 +208,7 @@ header {
cursor: pointer;
&:hover {
- color: darken($color: $gl-text-color, $amount: 30%);
+ color: $gl-header-nav-hover-color;
}
}
@@ -271,4 +281,5 @@ header {
float: left;
margin-right: 5px;
border-radius: 50%;
+ border: 1px solid $avatar-border;
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 5365b62e456..29d55c44699 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -41,6 +41,21 @@ body {
}
}
+ .alert-link-group {
+ float: right;
+ }
+
+ /* Center alert text and alert action links on smaller screens */
+ @media (max-width: $screen-sm-max) {
+ .alert {
+ text-align: center;
+ }
+
+ .alert-link-group {
+ float: none;
+ }
+ }
+
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning {
transition: background-color 0.15s, border-color 0.15s;
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 92d687996e9..8e2c56a8488 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -23,12 +23,16 @@
margin-right: 0;
}
- .issues-details-filters,
+ .issues-details-filters:not(.filtered-search-block),
.dash-projects-filters,
.check-all-holder {
display: none;
}
+ .issues-holder .issue-check {
+ display: none;
+ }
+
.rss-btn {
display: none;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index a292e7686f9..401c2d0f6ee 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -101,7 +101,7 @@
&:hover,
&:active,
&:focus {
- border-bottom: none;
+ border-color: transparent;
}
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 838f5442fff..f0b03710c79 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -236,9 +236,13 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) {
padding-right: $gutter_width;
- .merge-request-tabs-holder.affix {
+ &:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
}
+
+ &.with-overlay .merge-request-tabs-holder.affix {
+ right: $sidebar_collapsed_width;
+ }
}
&.with-overlay {
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 3e52c482ece..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;
@@ -102,6 +104,10 @@ $gl-text-red: #d12f19;
$gl-text-orange: #d90;
$gl-link-color: #3777b0;
$gl-grayish-blue: #7f8fa4;
+$gl-gray: $gl-text-color;
+$gl-gray-dark: #313236;
+$gl-header-color: #4c4e54;
+$gl-header-nav-hover-color: #434343;
/*
* Lists
@@ -172,6 +178,9 @@ $count-arrow-border: #dce0e5;
$save-project-loader-color: #555;
$divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc;
+$general-hover-transition-duration: 150ms;
+$general-hover-transition-curve: linear;
+
/*
* Common component specific colors
@@ -264,10 +273,16 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%);
/*
+* Filtered Search
+*/
+$dropdown-hover-color: #3b86ff;
+
+/*
* Buttons
*/
$btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed;
+$btn-white-active: #848484;
/*
* Badges
@@ -421,6 +436,7 @@ $help-shortcut-header-color: #333;
*/
$issues-today-bg: #f3fff2;
$issues-today-border: #e1e8d5;
+$compare-display-color: #888;
/*
* jQuery UI
@@ -525,4 +541,4 @@ Pipeline Graph
*/
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
-$action-icon-color: #d6d6d6;
+$action-icon-color: #d6d6d6; \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 6566f27ea2d..cda069e6c0e 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -20,6 +20,10 @@
.fa {
color: $cycle-analytics-light-gray;
+
+ &:hover {
+ color: $gl-text-color;
+ }
}
.stage-header {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 0a8c037c402..93cc5a8cf0a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,3 +1,52 @@
+// Limit MR description for side-by-side diff view
+.fixed-width-container {
+ max-width: $limited-layout-width - ($gl-padding * 2);
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.limit-container-width {
+ .detail-page-header {
+ @extend .fixed-width-container;
+ }
+
+ .issuable-details {
+ .detail-page-description,
+ .mr-source-target,
+ .mr-state-widget,
+ .merge-manually {
+ @extend .fixed-width-container;
+ }
+
+ .merge-request-tabs-holder {
+ &.affix {
+ border-bottom: 1px solid $border-color;
+
+ .nav-links {
+ border: 0;
+ }
+ }
+
+ .container-fluid {
+ @extend .fixed-width-container;
+ }
+ }
+ }
+
+ .merge-request-details {
+ .emoji-list-container {
+ @extend .fixed-width-container;
+ }
+ }
+
+ .diffs {
+ .mr-version-controls,
+ .files-changed {
+ @extend .fixed-width-container;
+ }
+ }
+}
+
.issuable-details {
section {
.issuable-discussion {
@@ -105,8 +154,8 @@
.edit-link {
color: $gl-text-color;
- &:hover {
- color: $md-link-color;
+ &:not([href]):hover {
+ color: rgba($avatar-border, .2);
}
}
}
@@ -283,6 +332,10 @@
&:hover {
color: $md-link-color;
text-decoration: none;
+
+ .avatar {
+ border-color: rgba($avatar-border, .2);
+ }
}
}
@@ -324,6 +377,10 @@
display: inline-block;
padding: 5px;
+ &:nth-of-type(7n) {
+ padding-right: 0;
+ }
+
.author_link {
display: block;
}
@@ -420,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/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 78683c7d574..21d9b4c54ea 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -203,6 +203,10 @@
z-index: 3;
border-radius: $label-border-radius;
padding: 6px 10px 6px 9px;
+
+ &:hover {
+ box-shadow: inset 0 0 0 80px $label-remove-border;
+ }
}
.btn {
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index e284b7269ce..686b64cdd24 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -109,6 +109,10 @@
.avatar {
float: none;
}
+
+ > a:not(:last-of-type) {
+ margin-right: 5px;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e2a0253da38..cbe38b60d60 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -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..8dff22e32bd 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) {
@@ -348,6 +349,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/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 8b1976bd925..722b3006f7c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -216,8 +216,8 @@
}
}
-.user-profile {
+.user-profile {
.cover-controls a {
margin-left: 5px;
}
@@ -231,8 +231,11 @@
}
}
- @media (max-width: $screen-xs-max) {
+ .user-profile-nav {
+ font-size: 0;
+ }
+ @media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
}
@@ -253,6 +256,12 @@
}
}
}
+
+ .user-profile-nav {
+ a {
+ margin-right: 0;
+ }
+ }
}
}
@@ -271,4 +280,4 @@ table.u2f-registrations {
.scopes-list {
padding-left: 18px;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 9455ba3b98a..cd0839e58ea 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 {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index cedd4cb2987..12bff32bbf3 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -14,6 +14,20 @@
}
}
+.search form:hover,
+.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%);
+}
+
+input[type="checkbox"]:hover {
+ box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
+}
+
.search {
margin-right: 10px;
margin-left: 10px;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index c2bb8464824..1b4987dd738 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -67,69 +67,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
- :default_projects_limit,
- :default_branch_protection,
- :signup_enabled,
- :signin_enabled,
- :require_two_factor_authentication,
- :two_factor_grace_period,
- :gravatar_enabled,
- :sign_in_text,
- :after_sign_up_text,
- :help_page_text,
- :home_page_url,
+ application_setting_params_ce
+ )
+ end
+
+ def application_setting_params_ce
+ [
+ :admin_notification_email,
:after_sign_out_path,
- :max_attachment_size,
- :session_expire_delay,
+ :after_sign_up_text,
+ :akismet_api_key,
+ :akismet_enabled,
+ :container_registry_token_expire_delay,
+ :default_branch_protection,
+ :default_group_visibility,
:default_project_visibility,
+ :default_projects_limit,
:default_snippet_visibility,
- :default_group_visibility,
- :domain_whitelist_raw,
:domain_blacklist_enabled,
- :domain_blacklist_raw,
:domain_blacklist_file,
- :version_check_enabled,
- :admin_notification_email,
- :user_oauth_applications,
- :user_default_external,
- :shared_runners_enabled,
- :shared_runners_text,
+ :domain_blacklist_raw,
+ :domain_whitelist_raw,
+ :email_author_in_body,
+ :enabled_git_access_protocol,
+ :gravatar_enabled,
+ :help_page_text,
+ :home_page_url,
+ :housekeeping_bitmaps_enabled,
+ :housekeeping_enabled,
+ :housekeeping_full_repack_period,
+ :housekeeping_gc_period,
+ :housekeeping_incremental_repack_period,
+ :html_emails_enabled,
+ :koding_enabled,
+ :koding_url,
+ :plantuml_enabled,
+ :plantuml_url,
:max_artifacts_size,
+ :max_attachment_size,
:metrics_enabled,
:metrics_host,
- :metrics_port,
- :metrics_pool_size,
- :metrics_timeout,
:metrics_method_call_threshold,
+ :metrics_packet_size,
+ :metrics_pool_size,
+ :metrics_port,
:metrics_sample_interval,
+ :metrics_timeout,
:recaptcha_enabled,
- :recaptcha_site_key,
:recaptcha_private_key,
- :sentry_enabled,
- :sentry_dsn,
- :akismet_enabled,
- :akismet_api_key,
- :koding_enabled,
- :koding_url,
- :email_author_in_body,
- :html_emails_enabled,
+ :recaptcha_site_key,
:repository_checks_enabled,
- :metrics_packet_size,
+ :require_two_factor_authentication,
+ :session_expire_delay,
+ :sign_in_text,
+ :signin_enabled,
+ :signup_enabled,
+ :sentry_dsn,
+ :sentry_enabled,
:send_user_confirmation_email,
- :container_registry_token_expire_delay,
- :enabled_git_access_protocol,
+ :shared_runners_enabled,
+ :shared_runners_text,
:sidekiq_throttling_enabled,
:sidekiq_throttling_factor,
- :housekeeping_enabled,
- :housekeeping_bitmaps_enabled,
- :housekeeping_incremental_repack_period,
- :housekeeping_full_repack_period,
- :housekeeping_gc_period,
+ :two_factor_grace_period,
+ :user_default_external,
+ :user_oauth_applications,
+ :version_check_enabled,
+
+ disabled_oauth_sign_in_sources: [],
+ import_sources: [],
repository_storages: [],
restricted_visibility_levels: [],
- import_sources: [],
- disabled_oauth_sign_in_sources: [],
sidekiq_throttling_queues: []
- )
+ ]
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index add1c819adf..b7722a1d15d 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -61,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(
+ params.require(:group).permit(group_params_ce)
+ end
+
+ def group_params_ce
+ [
:avatar,
:description,
:lfs_enabled,
@@ -69,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController
:path,
:request_access_enabled,
:visibility_level
- )
+ ]
end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index df9039b16b2..aa0f8d434dc 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -161,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController
@user ||= User.find_by!(username: params[:id])
end
- def user_params
- params.require(:user).permit(
- :email, :remember_me, :bio, :name, :username,
- :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
- :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
- :projects_limit, :can_create_group, :admin, :key_id, :external
- )
- end
-
def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options)
end
@@ -177,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController
def default_route
[:admin, @user]
end
+
+ def user_params
+ params.require(:user).permit(user_params_ce)
+ end
+
+ def user_params_ce
+ [
+ :admin,
+ :avatar,
+ :bio,
+ :can_create_group,
+ :color_scheme_id,
+ :email,
+ :extern_uid,
+ :external,
+ :force_random_password,
+ :hide_no_password,
+ :hide_no_ssh_key,
+ :key_id,
+ :linkedin,
+ :name,
+ :password_expires_at,
+ :projects_limit,
+ :provider,
+ :remember_me,
+ :skype,
+ :theme_id,
+ :twitter,
+ :username,
+ :website_url
+ ]
+ end
end
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/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 549a8526715..d7f5a4e4682 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -1,31 +1,72 @@
module ServiceParams
extend ActiveSupport::Concern
- ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
- :room, :recipients, :project_url, :webhook,
- :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
- :build_key, :server, :teamcity_url, :drone_url, :build_type,
- :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
- :colorize_messages, :channels,
- # We're using `issues_events` and `merge_requests_events`
- # in the view so we still need to explicitly state them
- # here. `Service#event_names` would only give
- # `issue_events` and `merge_request_events` (singular!)
- # See app/helpers/services_helper.rb for how we
- # make those event names plural as special case.
- :issues_events, :confidential_issues_events, :merge_requests_events,
- :notify_only_broken_builds, :notify_only_broken_pipelines,
- :add_pusher, :send_from_committer_email, :disable_diffs,
- :external_wiki_url, :notify, :color,
- :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
- :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace]
+ ALLOWED_PARAMS_CE = [
+ :active,
+ :add_pusher,
+ :api_key,
+ :api_url,
+ :api_version,
+ :bamboo_url,
+ :build_key,
+ :build_type,
+ :ca_pem,
+ :channel,
+ :channels,
+ :color,
+ :colorize_messages,
+ :confidential_issues_events,
+ :default_irc_uri,
+ :description,
+ :device,
+ :disable_diffs,
+ :drone_url,
+ :enable_ssl_verification,
+ :external_wiki_url,
+ # We're using `issues_events` and `merge_requests_events`
+ # in the view so we still need to explicitly state them
+ # here. `Service#event_names` would only give
+ # `issue_events` and `merge_request_events` (singular!)
+ # See app/helpers/services_helper.rb for how we
+ # make those event names plural as special case.
+ :issues_events,
+ :issues_url,
+ :jira_issue_transition_id,
+ :merge_requests_events,
+ :namespace,
+ :new_issue_url,
+ :notify,
+ :notify_only_broken_builds,
+ :notify_only_broken_pipelines,
+ :password,
+ :priority,
+ :project_key,
+ :project_url,
+ :recipients,
+ :restrict_to_branch,
+ :room,
+ :send_from_committer_email,
+ :server,
+ :server_host,
+ :server_port,
+ :sound,
+ :subdomain,
+ :teamcity_url,
+ :title,
+ :token,
+ :type,
+ :url,
+ :user_key,
+ :username,
+ :webhook
+ ]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
def service_params
dynamic_params = @service.event_channel_names + @service.event_names
- service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
+ service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params)
if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param|
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/groups_controller.rb b/app/controllers/groups_controller.rb
index b61f4e9a2db..f81237db991 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -125,7 +125,11 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(
+ params.require(:group).permit(group_params_ce)
+ end
+
+ def group_params_ce
+ [
:avatar,
:description,
:lfs_enabled,
@@ -135,7 +139,7 @@ class GroupsController < Groups::ApplicationController
:request_access_enabled,
:share_with_group_lock,
:visibility_level
- )
+ ]
end
def load_events
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/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 6004e7d7115..9ac5bf4b9f8 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -347,6 +347,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def merge_widget_refresh
+ if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
+ @status = :success
+ elsif merge_request.merge_when_build_succeeds
+ @status = :merge_when_build_succeeds
+ end
+
+ render 'merge'
+ end
+
def branch_from
# This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
@@ -409,10 +419,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else
ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
-
- if ci_service.respond_to?(:commit_coverage)
- coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch)
- end
end
response = {
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index b71509f2c9b..c5d93ce25bc 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController
end
def create
- @note = Notes::CreateService.new(project, current_user, note_params).execute
+ create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha])
+ @note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
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/projects_controller.rb b/app/controllers/projects_controller.rb
index d5ee503c44c..444ff837bb3 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController
end
def update
- status = ::Projects::UpdateService.new(@project, current_user, project_params).execute
+ result = ::Projects::UpdateService.new(@project, current_user, project_params).execute
# Refresh the repo in case anything changed
- @repository = project.repository
+ @repository = @project.repository
respond_to do |format|
- if status
+ if result[:status] == :success
flash[:notice] = "Project '#{@project.name}' was successfully updated."
format.html do
- redirect_to(
- edit_project_path(@project),
- notice: "Project '#{@project.name}' was successfully updated."
- )
+ redirect_to(edit_project_path(@project))
end
else
format.html { render 'edit' }
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b4c14d05eaf..1576fc80a6b 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -165,31 +165,53 @@ class IssuableFinder
end
end
- def assignee?
- params[:assignee_id].present?
+ def assignee_id?
+ params[:assignee_id].present? && params[:assignee_id] != NONE
+ end
+
+ def assignee_username?
+ params[:assignee_username].present? && params[:assignee_username] != NONE
+ end
+
+ def no_assignee?
+ # Assignee_id takes precedence over assignee_username
+ params[:assignee_id] == NONE || params[:assignee_username] == NONE
end
def assignee
return @assignee if defined?(@assignee)
@assignee =
- if assignee? && params[:assignee_id] != NONE
- User.find(params[:assignee_id])
+ if assignee_id?
+ User.find_by(id: params[:assignee_id])
+ elsif assignee_username?
+ User.find_by(username: params[:assignee_username])
else
nil
end
end
- def author?
- params[:author_id].present?
+ def author_id?
+ params[:author_id].present? && params[:author_id] != NONE
+ end
+
+ def author_username?
+ params[:author_username].present? && params[:author_username] != NONE
+ end
+
+ def no_author?
+ # author_id takes precedence over author_username
+ params[:author_id] == NONE || params[:author_username] == NONE
end
def author
return @author if defined?(@author)
@author =
- if author? && params[:author_id] != NONE
- User.find(params[:author_id])
+ if author_id?
+ User.find_by(id: params[:author_id])
+ elsif author_username?
+ User.find_by(username: params[:author_username])
else
nil
end
@@ -263,16 +285,24 @@ class IssuableFinder
end
def by_assignee(items)
- if assignee?
- items = items.where(assignee_id: assignee.try(:id))
+ if assignee
+ items = items.where(assignee_id: assignee.id)
+ elsif no_assignee?
+ items = items.where(assignee_id: nil)
+ elsif assignee_id? || assignee_username? # assignee not found
+ items = items.none
end
items
end
def by_author(items)
- if author?
- items = items.where(author_id: author.try(:id))
+ if author
+ items = items.where(author_id: author.id)
+ elsif no_author?
+ items = items.where(author_id: nil)
+ elsif author_id? || author_username? # author not found
+ items = items.none
end
items
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index c816b616631..a112928c6de 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -244,7 +244,9 @@ module ApplicationHelper
scope: params[:scope],
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
+ assignee_username: params[:assignee_username],
author_id: params[:author_id],
+ author_username: params[:author_username],
search: params[:search],
label_name: params[:label_name]
}
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index c35d6611ab0..aed1d7c839f 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -165,4 +165,10 @@ module DiffHelper
link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class]
end
+
+ def render_overflow_warning?(diff_files)
+ diffs = @merge_request_diff.presence || diff_files
+
+ diffs.overflow?
+ 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/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 20218775659..8c2c4e8833b 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -19,6 +19,14 @@ module MergeRequestsHelper
}
end
+ def mr_widget_refresh_url(mr)
+ if mr && mr.source_project
+ merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
+ else
+ ''
+ end
+ end
+
def mr_css_classes(mr)
classes = "merge-request"
classes << " closed" if mr.closed?
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/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/models/application_setting.rb b/app/models/application_setting.rb
index bf463a3b6bb..8fab77cda0a 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :koding_enabled
+ validates :plantuml_url,
+ presence: true,
+ if: :plantuml_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -184,6 +188,8 @@ class ApplicationSetting < ActiveRecord::Base
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,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 27042798741..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)
@@ -507,6 +514,10 @@ module Ci
end
end
+ def has_expiring_artifacts?
+ artifacts_expire_at.present?
+ end
+
def keep_artifacts!
self.update(artifacts_expire_at: nil)
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/commit.rb b/app/models/commit.rb
index 0b924b063a4..5d942cb0422 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -318,6 +318,20 @@ class Commit
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end
+ def persisted?
+ true
+ end
+
+ def touch
+ # 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/commit_status.rb b/app/models/commit_status.rb
index 31cd381dcd2..9547c57b2ae 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -137,4 +137,10 @@ class CommitStatus < ActiveRecord::Base
.new(self, current_user)
.fabricate!
end
+
+ def sortable_name
+ name.split(/(\d+)/).map do |v|
+ v =~ /\d+/ ? v.to_i : v
+ end
+ end
end
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/milestoneish.rb b/app/models/concerns/milestoneish.rb
index fcc8feddb39..e9450dd0c26 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -7,11 +7,14 @@ module Milestoneish
def total_items_count(user)
memoize_per_user(user, :total_items_count) do
- issues_count = count_issues_by_state(user).values.sum
- issues_count + merge_requests.size
+ total_issues_count(user) + merge_requests.size
end
end
+ def total_issues_count(user)
+ count_issues_by_state(user).values.sum
+ end
+
def complete?(user)
total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user)
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/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 6d88951c713..60734bc6660 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility
build_project_feature unless project_feature
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
- project_feature.update_attribute(field, access_level)
+ project_feature.send(:write_attribute, field, access_level)
end
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 944519a3070..2589215ad19 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -55,30 +55,30 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
- def calculate_reactive_cache
+ def calculate_reactive_cache(*args)
raise NotImplementedError
end
- def with_reactive_cache(&blk)
- within_reactive_cache_lifetime do
- data = Rails.cache.read(full_reactive_cache_key)
+ def with_reactive_cache(*args, &blk)
+ within_reactive_cache_lifetime(*args) do
+ data = Rails.cache.read(full_reactive_cache_key(*args))
yield data if data.present?
end
ensure
- Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime)
- ReactiveCachingWorker.perform_async(self.class, id)
+ Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
+ ReactiveCachingWorker.perform_async(self.class, id, *args)
end
- def clear_reactive_cache!
- Rails.cache.delete(full_reactive_cache_key)
+ def clear_reactive_cache!(*args)
+ Rails.cache.delete(full_reactive_cache_key(*args))
end
- def exclusively_update_reactive_cache!
- locking_reactive_cache do
- within_reactive_cache_lifetime do
- enqueuing_update do
- value = calculate_reactive_cache
- Rails.cache.write(full_reactive_cache_key, value)
+ def exclusively_update_reactive_cache!(*args)
+ locking_reactive_cache(*args) do
+ within_reactive_cache_lifetime(*args) do
+ enqueuing_update(*args) do
+ value = calculate_reactive_cache(*args)
+ Rails.cache.write(full_reactive_cache_key(*args), value)
end
end
end
@@ -93,22 +93,26 @@ module ReactiveCaching
([prefix].flatten + qualifiers).join(':')
end
- def locking_reactive_cache
- lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout)
+ def alive_reactive_cache_key(*qualifiers)
+ full_reactive_cache_key(*(qualifiers + ['alive']))
+ end
+
+ def locking_reactive_cache(*args)
+ lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout)
uuid = lease.try_obtain
yield if uuid
ensure
- Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid)
+ Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
end
- def within_reactive_cache_lifetime
- yield if Rails.cache.read(full_reactive_cache_key('alive'))
+ def within_reactive_cache_lifetime(*args)
+ yield if Rails.cache.read(alive_reactive_cache_key(*args))
end
- def enqueuing_update
+ def enqueuing_update(*args)
yield
ensure
- ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id)
+ ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
end
end
end
diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb
new file mode 100644
index 00000000000..e1f868a299b
--- /dev/null
+++ b/app/models/concerns/reactive_service.rb
@@ -0,0 +1,10 @@
+module ReactiveService
+ extend ActiveSupport::Concern
+
+ included do
+ include ReactiveCaching
+
+ # Default cache key: class name + project_id
+ self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
+ end
+end
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/concerns/valid_attribute.rb b/app/models/concerns/valid_attribute.rb
new file mode 100644
index 00000000000..8c35cea8d58
--- /dev/null
+++ b/app/models/concerns/valid_attribute.rb
@@ -0,0 +1,10 @@
+module ValidAttribute
+ extend ActiveSupport::Concern
+
+ # Checks whether an attribute has failed validation or not
+ #
+ # +attribute+ The symbolised name of the attribute i.e :name
+ def valid_attribute?(attribute)
+ self.errors.empty? || self.errors.messages[attribute].nil?
+ 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 82f53d17ddd..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(git --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/forked_project_link.rb b/app/models/forked_project_link.rb
index 9803bae0bee..36cf7ad6a28 100644
--- a/app/models/forked_project_link.rb
+++ b/app/models/forked_project_link.rb
@@ -1,4 +1,4 @@
class ForkedProjectLink < ActiveRecord::Base
- belongs_to :forked_to_project, class_name: Project
- belongs_to :forked_from_project, class_name: Project
+ belongs_to :forked_to_project, class_name: 'Project'
+ belongs_to :forked_from_project, class_name: 'Project'
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 70005a87f4b..cd5b345bae5 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?]
@@ -898,10 +902,22 @@ class MergeRequest < ActiveRecord::Base
end
def has_commits?
- commits_count > 0
+ merge_request_diff && commits_count > 0
end
def has_no_commits?
!has_commits?
end
+
+ def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil)
+ return false unless can_be_merged_by?(current_user)
+
+ return true if autocomplete_precheck
+
+ return false unless mergeable?(skip_ci_check: true)
+ return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
+ return false if last_diff_sha != diff_head_sha
+
+ true
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 64dd586c9e0..dadb81f9b6e 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base
# and save it as array of hashes in st_diffs db field
def save_diffs
new_attributes = {}
- new_diffs = []
if commits.size.zero?
new_attributes[:state] = :empty
else
diff_collection = compare.diffs(Commit.max_diff_options)
-
- if diff_collection.overflow?
- # Set our state to 'overflow' to make the #empty? and #collected?
- # methods (generated by StateMachine) return false.
- new_attributes[:state] = :overflow
- end
-
- new_attributes[:real_size] = diff_collection.real_size
+ new_attributes[:real_size] = compare.diffs.real_size
if diff_collection.any?
new_diffs = dump_diffs(diff_collection)
new_attributes[:state] = :collected
end
+
+ new_attributes[:st_diffs] = new_diffs || []
+
+ # Set our state to 'overflow' to make the #empty? and #collected?
+ # methods (generated by StateMachine) return false.
+ #
+ # This attribution has to come at the end of the method so 'overflow'
+ # state does not get overridden by 'collected'.
+ new_attributes[:state] = :overflow if diff_collection.overflow?
end
- new_attributes[:st_diffs] = new_diffs
update_columns_serialized(new_attributes)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 94a6f3ba799..1630975b0d3 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -12,6 +12,7 @@ class Project < ActiveRecord::Base
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
+ include ValidAttribute
include ProjectFeaturesCompatibility
include SelectForProjectAuthorization
include Routable
@@ -65,6 +66,8 @@ class Project < ActiveRecord::Base
end
end
+ after_validation :check_pending_delete
+
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
@@ -119,7 +122,7 @@ 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 :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
@@ -1029,7 +1032,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
- repository.expire_avatar_cache
+ repository.after_change_head
reload_default_branch
end
@@ -1320,4 +1323,21 @@ class Project < ActiveRecord::Base
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
end
+
+ def check_pending_delete
+ return if valid_attribute?(:name) && valid_attribute?(:path)
+ return unless pending_delete_twin
+
+ %i[route route.path name path].each do |error|
+ errors.delete(error)
+ end
+
+ errors.add(:base, "The project is still being deleted. Please try again later.")
+ end
+
+ def pending_delete_twin
+ return false unless path
+
+ Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace)
+ end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index b5c76e4d4fe..4819bdbef8c 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -1,4 +1,6 @@
class BambooService < CiService
+ include ReactiveService
+
prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated?
@@ -58,31 +60,46 @@ class BambooService < CiService
%w(push)
end
- def build_info(sha)
- @response = get_path("rest/api/latest/result?label=#{sha}")
+ def build_page(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
- def build_page(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
- if @response.code != 200 || @response['results']['results']['size'] == '0'
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
+ get_path("updateAndBuild.action?buildKey=#{build_key}")
+ end
+
+ def calculate_reactive_cache(sha, ref)
+ response = get_path("rest/api/latest/result?label=#{sha}")
+
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
+ end
+
+ private
+
+ def read_build_page(response)
+ if response.code != 200 || response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page.
URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else
# If actual build link is available, go to build result page.
- result_key = @response['results']['results']['result']['planResultKey']['key']
+ result_key = response['results']['results']['result']['planResultKey']['key']
URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end
end
- def commit_status(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
- return :error unless @response.code == 200 || @response.code == 404
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
- status = if @response.code == 404 || @response['results']['results']['size'] == '0'
+ status = if response.code == 404 || response['results']['results']['size'] == '0'
'Pending'
else
- @response['results']['results']['result']['buildState']
+ response['results']['results']['result']['buildState']
end
if status.include?('Success')
@@ -96,14 +113,6 @@ class BambooService < CiService
end
end
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- get_path("updateAndBuild.action?buildKey=#{build_key}")
- end
-
- private
-
def build_url(path)
URI.join("#{bamboo_url}/", path).to_s
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index fe6d7aabb22..e77942d8f3c 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -1,6 +1,8 @@
require "addressable/uri"
class BuildkiteService < CiService
+ include ReactiveService
+
ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token
@@ -33,13 +35,7 @@ class BuildkiteService < CiService
end
def commit_status(sha, ref)
- response = HTTParty.get(commit_status_path(sha), verify: false)
-
- if response.code == 200 && response['status']
- response['status']
- else
- :error
- end
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
def commit_status_path(sha)
@@ -78,6 +74,19 @@ class BuildkiteService < CiService
]
end
+ def calculate_reactive_cache(sha, ref)
+ response = HTTParty.get(commit_status_path(sha), verify: false)
+
+ status =
+ if response.code == 200 && response['status']
+ response['status']
+ else
+ :error
+ end
+
+ { commit_status: status }
+ end
+
private
def webhook_token
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index b7ef44c3054..3098625b0c2 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -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/ci_service.rb b/app/models/project_services/ci_service.rb
index 596c00705ad..4de0106707e 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -12,15 +12,7 @@ class CiService < Service
%w(push)
end
- def merge_request_page(iid, sha, ref)
- commit_page(sha, ref)
- end
-
- def commit_page(sha, ref)
- build_page(sha, ref)
- end
-
- # Return complete url to merge_request page
+ # Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
@@ -35,23 +27,6 @@ class CiService < Service
#
#
# Ex.
- # @service.merge_request_status(9, '13be4ac', 'dev')
- # # => 'success'
- #
- # @service.merge_request_status(10, '2abe4ac', 'dev)
- # # => 'running'
- #
- #
- def merge_request_status(iid, sha, ref)
- commit_status(sha, ref)
- end
-
- # Return string with build status or :error symbol
- #
- # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
- #
- #
- # Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index adc78a427ee..4bbbebf54cb 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -1,4 +1,6 @@
class DroneCiService < CiService
+ include ReactiveService
+
prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
@@ -34,14 +36,6 @@ class DroneCiService < CiService
%w(push merge_request tag_push)
end
- def merge_request_status_path(iid, sha = nil, ref = nil)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
- "?access_token=#{token}"]
-
- URI.join(*url).to_s
- end
-
def commit_status_path(sha, ref)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
@@ -50,54 +44,34 @@ class DroneCiService < CiService
URI.join(*url).to_s
end
- def merge_request_status(iid, sha, ref)
- response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification)
-
- if response.code == 200 and response['status']
- case response['status']
- when 'killed'
- :canceled
- when 'failure', 'error'
- # Because drone return error if some test env failed
- :failed
- else
- response["status"]
- end
- else
- :error
- end
- rescue Errno::ECONNREFUSED
- :error
+ def commit_status(sha, ref)
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
- def commit_status(sha, ref)
+ def calculate_reactive_cache(sha, ref)
response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
- if response.code == 200 and response['status']
- case response['status']
- when 'killed'
- :canceled
- when 'failure', 'error'
- # Because drone return error if some test env failed
- :failed
+ status =
+ if response.code == 200 and response['status']
+ case response['status']
+ when 'killed'
+ :canceled
+ when 'failure', 'error'
+ # Because drone return error if some test env failed
+ :failed
+ else
+ response["status"]
+ end
else
- response["status"]
+ :error
end
- else
- :error
- end
- rescue Errno::ECONNREFUSED
- :error
- end
- def merge_request_page(iid, sha, ref)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
-
- URI.join(*url).to_s
+ { commit_status: status }
+ rescue Errno::ECONNREFUSED
+ { commit_status: :error }
end
- def commit_page(sha, ref)
+ def build_page(sha, ref)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"]
@@ -105,14 +79,6 @@ class DroneCiService < CiService
URI.join(*url).to_s
end
- def commit_coverage(sha, ref)
- nil
- end
-
- def build_page(sha, ref)
- commit_page(sha, ref)
- end
-
def title
'Drone CI'
end
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index ee8a0b55275..2bea3827b95 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -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/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index a4a967c9bc9..6726082048f 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -1,4 +1,6 @@
class TeamcityService < CiService
+ include ReactiveService
+
prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated?
@@ -61,43 +63,18 @@ class TeamcityService < CiService
]
end
- def build_info(sha)
- @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
- end
-
def build_page(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
-
- if @response.code != 200
- # If actual build link can't be determined,
- # send user to build summary page.
- build_url("viewLog.html?buildTypeId=#{build_type}")
- else
- # If actual build link is available, go to build result page.
- built_id = @response['build']['id']
- build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
- end
+ with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
def commit_status(sha, ref)
- build_info(sha) if @response.nil? || !@response.code
- return :error unless @response.code == 200 || @response.code == 404
+ with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
+ end
- status = if @response.code == 404
- 'Pending'
- else
- @response['build']['status']
- end
+ def calculate_reactive_cache(sha, ref)
+ response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
- if status.include?('SUCCESS')
- 'success'
- elsif status.include?('FAILURE')
- 'failed'
- elsif status.include?('Pending')
- 'pending'
- else
- :error
- end
+ { build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
def execute(data)
@@ -122,6 +99,40 @@ class TeamcityService < CiService
private
+ def read_build_page(response)
+ if response.code != 200
+ # If actual build link can't be determined,
+ # send user to build summary page.
+ build_url("viewLog.html?buildTypeId=#{build_type}")
+ else
+ # If actual build link is available, go to build result page.
+ built_id = response['build']['id']
+ build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
+ end
+ end
+
+ def read_commit_status(response)
+ return :error unless response.code == 200 || response.code == 404
+
+ status = if response.code == 404
+ 'Pending'
+ else
+ response['build']['status']
+ end
+
+ return :error unless status.present?
+
+ if status.include?('SUCCESS')
+ 'success'
+ elsif status.include?('FAILURE')
+ 'failed'
+ elsif status.include?('Pending')
+ 'pending'
+ else
+ :error
+ end
+ end
+
def build_url(path)
URI.join("#{teamcity_url}/", path).to_s
end
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/repository.rb b/app/models/repository.rb
index 3266e9c75f0..43dba86e5ed 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -439,6 +439,11 @@ class Repository
expire_content_cache
end
+ # Runs code after the HEAD of a repository is changed.
+ def after_change_head
+ expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
+ end
+
# Runs code after a repository has been forked/imported.
def after_import
expire_content_cache
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/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/build_action_entity.rb b/app/serializers/build_action_entity.rb
index 3e72892d584..184f5fd4b52 100644
--- a/app/serializers/build_action_entity.rb
+++ b/app/serializers/build_action_entity.rb
@@ -2,7 +2,7 @@ class BuildActionEntity < Grape::Entity
include RequestAwareEntity
expose :name do |build|
- build.name.humanize
+ build.name
end
expose :path do |build|
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index 49f4db36295..31763955f97 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -8,16 +8,16 @@ class CommitEntity < API::Entities::RepoCommit
end
expose :commit_url do |commit|
- namespace_project_tree_url(
+ namespace_project_commit_url(
request.project.namespace,
request.project,
- id: commit.id)
+ commit)
end
expose :commit_path do |commit|
- namespace_project_tree_path(
+ namespace_project_commit_path(
request.project.namespace,
request.project,
- id: commit.id)
+ commit)
end
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/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 0a9563ed7e7..51d5d7563fc 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
@@ -136,6 +137,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|
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index ad16ef8c70f..3cb9aae83f6 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ merge_from_slash_command(merge_request) if params[:merge]
+
if merge_request.closed_without_fork?
params.except!(:target_branch, :force_remove_source_branch)
end
@@ -69,6 +71,19 @@ module MergeRequests
end
end
+ def merge_from_slash_command(merge_request)
+ last_diff_sha = params.delete(:merge)
+ return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha)
+
+ merge_request.update(merge_error: nil)
+
+ if merge_request.head_pipeline && merge_request.head_pipeline.active?
+ MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
+ else
+ MergeWorker.perform_async(merge_request.id, current_user.id, {})
+ end
+ end
+
def reopen_service
MergeRequests::ReopenService
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 1beca9f4109..cdd765c85eb 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,6 +1,8 @@
module Notes
class CreateService < BaseService
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
@@ -19,7 +21,8 @@ module Notes
slash_commands_service = SlashCommandsService.new(project, current_user)
if slash_commands_service.supported?(note)
- content, command_params = slash_commands_service.extract_commands(note)
+ options = { merge_request_diff_head_sha: merge_request_diff_head_sha }
+ content, command_params = slash_commands_service.extract_commands(note, options)
only_commands = content.empty?
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
index 2edbd39a9e7..aaea9717fc4 100644
--- a/app/services/notes/slash_commands_service.rb
+++ b/app/services/notes/slash_commands_service.rb
@@ -19,10 +19,10 @@ module Notes
self.class.supported?(note, current_user)
end
- def extract_commands(note)
+ def extract_commands(note, options = {})
return [note.note, {}] unless supported?(note)
- SlashCommands::InterpretService.new(project, current_user).
+ SlashCommands::InterpretService.new(project, current_user, options).
execute(note.note, note.noteable)
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 8a6af8d8ada..842e23eb6b6 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -9,7 +9,7 @@ module Projects
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility)
- return project
+ return error('Visibility level unallowed')
end
end
@@ -23,6 +23,10 @@ module Projects
if project.previous_changes.include?('path')
project.rename_repo
end
+
+ success
+ else
+ error('Project could not be updated')
end
end
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index d75c5b1800e..3566a8ba92f 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -2,7 +2,7 @@ module SlashCommands
class InterpretService < BaseService
include Gitlab::SlashCommands::Dsl
- attr_reader :issuable
+ attr_reader :issuable, :options
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
@@ -13,7 +13,8 @@ module SlashCommands
opts = {
issuable: issuable,
current_user: current_user,
- project: project
+ project: project,
+ params: params
}
content, commands = extractor.extract_commands(content, opts)
@@ -58,6 +59,17 @@ module SlashCommands
@updates[:state_event] = 'reopen'
end
+ desc 'Merge (when build succeeds)'
+ condition do
+ last_diff_sha = params && params[:merge_request_diff_head_sha]
+ issuable.is_a?(MergeRequest) &&
+ issuable.persisted? &&
+ issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha)
+ end
+ command :merge do
+ @updates[:merge] = params[:merge_request_diff_head_sha]
+ end
+
desc 'Change title'
params '<New title>'
condition do
@@ -243,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/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 21ec1bd9e65..2d211d5ebbe 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -26,8 +26,26 @@ module Users
user.reload
end
- # This method returns the updated User object.
def execute
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. If we don't do so we may end up
+ # not updating the list of authorized projects properly. To prevent
+ # hammering Redis too much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ begin
+ execute_without_lease
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+ end
+
+ # This method returns the updated User object.
+ def execute_without_lease
current = current_authorizations_per_project
fresh = fresh_access_levels_per_project
@@ -47,26 +65,7 @@ module Users
end
end
- update_with_lease(remove, add)
- end
-
- # Updates the list of authorizations using an exclusive lease.
- def update_with_lease(remove = [], add = [])
- lease_key = "refresh_authorized_projects:#{user.id}"
- lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
-
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. If we don't do so we may end up
- # not updating the list of authorized projects properly. To prevent
- # hammering Redis too much we'll wait for a bit between retries.
- sleep(1)
- end
-
- begin
- update_authorizations(remove, add)
- ensure
- Gitlab::ExclusiveLease.cancel(lease_key, uuid)
- end
+ update_authorizations(remove, add)
end
# Updates the list of authorizations for the current user.
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 4612a7a058a..558bbe07b16 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -421,6 +421,23 @@
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
%fieldset
+ %legend PlantUML
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :plantuml_enabled do
+ = f.check_box :plantuml_enabled
+ Enable PlantUML
+ .form-group
+ = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .help-block
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
+
+ %fieldset
%legend Usage statistics
.form-group
.col-sm-offset-2.col-sm-10
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/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/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index bb4effeeeb1..60a561c9f9c 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -19,7 +19,7 @@
Your New Personal Access Token
.form-group
= text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block"
- = clipboard_button(clipboard_text: flash[:personal_access_token])
+ = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left")
%span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
%hr
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 2385a90401e..c0c82cde2f6 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,8 @@
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
- = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
+ = link_to avatar_icon(@user, 400), target: '_blank' do
+ = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0
Upload new avatar
.prepend-top-5.append-bottom-10
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/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 2eb49685f08..04efc2e996c 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -17,7 +17,6 @@
- if @project.protected_branch? branch.name
%span.label.label-success
- %i.fa.fa-lock
protected
.controls.hidden-xs
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 0b3adcbe121..37bf085130a 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -22,14 +22,14 @@
%p.build-detail-row
The artifacts were removed
#{time_ago_with_tooltip(@build.artifacts_expire_at)}
- - elsif @build.artifacts_expire_at
+ - elsif @build.has_expiring_artifacts?
%p.build-detail-row
The artifacts will be removed in
%span.js-artifacts-remove= @build.artifacts_expire_at
- if @build.artifacts?
.btn-group.btn-group-justified{ role: :group }
- - if @build.artifacts_expire_at
+ - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
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 6ce586cc8f6..990bfbcf951 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -86,7 +86,7 @@
%li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
- %span= build.name.humanize
+ %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' }
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index 12e4280d344..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 send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), 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/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index ab4a2dc36e5..58c20e225c6 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -18,8 +18,8 @@
= parallel_diff_btn
= render 'projects/diffs/stats', diff_files: diff_files
-- if diff_files.overflow?
- = render 'projects/diffs/warning', diff_files: diff_files
+- if render_overflow_warning?(diff_files)
+ = render 'projects/diffs/warning', diff_files: diffs
.files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file|
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 15df2edefc7..c37a33bbcd5 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,5 +1,5 @@
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
- .file-title{ id: "file-path-#{hexdigest(diff_file.file_path)}" }
+ .file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule?
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/empty.html.haml b/app/views/projects/empty.html.haml
index 3525a07a687..58c085cdb9d 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -52,7 +52,7 @@
git push -u origin master
%fieldset
- %h5 Existing folder or Git repository
+ %h5 Existing folder
%pre.light-well
:preserve
cd existing_folder
@@ -62,6 +62,15 @@
git commit
git push -u origin master
+ %fieldset
+ %h5 Existing Git repository
+ %pre.light-well
+ :preserve
+ cd existing_repo
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git push -u origin --all
+ git push -u origin --tags
+
- if can? current_user, :remove_project, @project
.prepend-top-20
= link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
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/index.html.haml b/app/views/projects/issues/index.html.haml
index 26f3f0ac292..18e8372ecab 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -6,6 +6,9 @@
= content_for :sub_nav do
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js')
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
@@ -20,7 +23,6 @@
= icon('rss')
%span.icon-label
Subscribe
- = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace,
@project,
@@ -30,7 +32,7 @@
title: "New Issue",
id: "new_issue_link" do
New Issue
- = render 'shared/issuable/filter', type: :issues
+ = render 'shared/issuable/search_bar', type: :issues
.issues-holder
= render 'issues'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 981bf640a6b..9fa00811af0 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,7 +1,9 @@
-- @content_class = "limit-container-width"
+- @content_class = "limit-container-width" unless fluid_layout
- 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/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml
index 24e86b8497f..a80f9aa4c4a 100644
--- a/app/views/projects/mattermosts/_team_selection.html.haml
+++ b/app/views/projects/mattermosts/_team_selection.html.haml
@@ -7,20 +7,21 @@
%p
= @teams.one? ? 'The team' : 'Select the team'
where the slash commands will be used in
- - selected_id = @teams.keys.first if @teams.one?
+ - selected_id = @teams.one? ? @teams.keys.first : 0
- options = mattermost_teams_options(@teams)
- options = options_for_select(options, selected_id)
- = f.select(:team_id, options, {}, { class: 'form-control', selected: "#{selected_id}" })
+ = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id })
+ = f.hidden_field(:team_id, value: selected_id) if @teams.one?
.help-block
- if @teams.one?
- This is the only team where you are an administrator.
+ This is the only available team.
- else
- The list shows teams where you are administrator
- To create a team, ask your Mattermost system administrator.
+ The list shows all available teams.
To create a team,
= link_to "#{Gitlab.config.mattermost.host}/create_team" do
use Mattermost's interface
= icon('external-link')
+ or ask your Mattermost system administrator.
%hr
%h4 Command trigger word
%p Choose the word that will trigger commands
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 1f63803c24e..2a7cd3a19d0 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,8 +1,9 @@
-- @content_class = "limit-container-width"
+- @content_class = "limit-container-width" unless fluid_layout
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- 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) }
@@ -47,7 +48,7 @@
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- .content-block.content-block-small
+ .content-block.content-block-small.emoji-list-container
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
@@ -112,3 +113,6 @@
merge_request = new MergeRequest({
action: "#{controller.action_name}"
});
+
+ var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@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/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 99c71e1454a..5f048d04b27 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,13 +1,5 @@
-- if @merge_request_diff.collected?
+- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions'
= render "projects/diffs/diffs", diffs: @diffs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
-- else
- .alert.alert-warning
- %h4
- Changes view for this comparison is extremely large.
- %p
- You can
- = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink"
- instead.
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index ec76c6a5417..93ed4b68e0e 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -8,7 +8,7 @@
%p
%strong Step 1.
Fetch and check out the branch for this merge request
- = clipboard_button(clipboard_target: "pre#merge-info-1")
+ = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard")
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
@@ -25,7 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
- = clipboard_button(clipboard_target: "pre#merge-info-3")
+ = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard")
%pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
@@ -38,7 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
- = clipboard_button(clipboard_target: "pre#merge-info-4")
+ = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard")
%pre.dark#merge-info-4
:preserve
git push origin #{h @merge_request.target_branch}
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/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 39731668a61..b561052e721 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -3,6 +3,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
+ = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
= note_target_fields(@note)
= f.hidden_field :commit_id
= f.hidden_field :line_code
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index abea6932567..df36279ed75 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -64,5 +64,4 @@
.vue-pipelines-index
-= page_specific_javascript_tag('vue_pagination/index.js')
= page_specific_javascript_tag('vue_pipelines_index/index.js')
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/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
index d9d392fa02f..4ee30b023ac 100644
--- a/app/views/projects/stage/_graph.html.haml
+++ b/app/views/projects/stage/_graph.html.haml
@@ -1,6 +1,6 @@
- stage = local_assigns.fetch(:stage)
- statuses = stage.statuses.latest
-- status_groups = statuses.sort_by(&:name).group_by(&:group_name)
+- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name)
%li.stage-column
.stage-name
%a{ name: stage.name }
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/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 96b75440309..03684389742 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -19,7 +19,7 @@
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
.input-group-btn
- = clipboard_button(clipboard_target: '#project_clone')
+ = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard")
:javascript
$('ul.clone-options-dropdown a').on('click',function(e){
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index a43bf33751a..ed6fc76c61e 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password?
- .no-password-message.alert.alert-warning.hidden-xs
+ .no-password-message.alert.alert-warning
You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account
- .pull-right
+ .alert-link-group
= link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put
|
= link_to 'Remind later', '#', class: 'hide-no-password-message'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index bb5fff2d3bb..d663fa13d10 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
- .no-ssh-key-message.alert.alert-warning.hidden-xs
+ .no-ssh-key-message.alert.alert-warning
You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile
- .pull-right
+ .alert-link-group
= link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link'
|
= link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link'
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
new file mode 100644
index 00000000000..44152319736
--- /dev/null
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -0,0 +1,141 @@
+- type = local_assigns.fetch(:type)
+
+.issues-filters
+ .issues-details-filters.row-content-block.second-block.filtered-search-block
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
+ - if @bulk_edit
+ .check-all-holder
+ = check_box_tag "check_all_issues", nil, false,
+ 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, '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' => '' }
+ %button.btn.btn-link
+ = icon('search')
+ %span
+ Keep typing and press Enter
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link
+ -# Encapsulate static class name `{{icon}}` inside #{} to bypass
+ -# haml lint's ClassAttributeWithStaticValue
+ %i.fa{ class: "#{'{{icon}}'}" }
+ %span.js-filter-hint
+ {{hint}}
+ %span.js-filter-tag.dropdown-light-content
+ {{tag}}
+ #js-dropdown-author.dropdown-menu
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.dropdown-user
+ %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ .dropdown-user-details
+ %span
+ {{name}}
+ %span.dropdown-light-content
+ @{{username}}
+ #js-dropdown-assignee.dropdown-menu
+ %ul{ 'data-dropdown' => true }
+ %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
+ %button.btn.btn-link.dropdown-user
+ %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' }
+ .dropdown-user-details
+ %span
+ {{name}}
+ %span.dropdown-light-content
+ @{{username}}
+ #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true }
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Milestone
+ %li.filter-dropdown-item{ 'data-value' => 'upcoming' }
+ %button.btn.btn-link
+ Upcoming
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value
+ {{title}}
+ #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true }
+ %ul{ 'data-dropdown' => true }
+ %li.filter-dropdown-item{ 'data-value' => 'none' }
+ %button.btn.btn-link
+ No Label
+ %li.divider
+ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true }
+ %li.filter-dropdown-item
+ %button.btn.btn-link
+ %span.dropdown-label-box{ style: 'background: {{color}}' }
+ %span.label-title.js-data-value
+ {{title}}
+ .pull-right
+ = render 'shared/sort_dropdown'
+
+ - if @bulk_edit
+ .issues_bulk_update.hide
+ = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
+ .filter-item.inline
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ %ul
+ %li
+ %a{ href: "#", data: { id: "reopen" } } Open
+ %li
+ %a{ href: "#", data: { id: "close" } } Closed
+ .filter-item.inline
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ .filter-item.inline
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+ .filter-item.inline.labels-filter
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ .filter-item.inline
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ %ul
+ %li
+ %a{ href: "#", data: { id: "subscribe" } } Subscribe
+ %li
+ %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
+
+ = hidden_field_tag 'update[issuable_ids]', []
+ = hidden_field_tag :state_event, params[:state_event]
+ .filter-item.inline
+ = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
+
+:javascript
+ new UsersSelect();
+ new LabelsSelect();
+ 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 5f199301364..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
@@ -153,15 +161,17 @@
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
.sidebar-collapsed-icon.dont-change-state
- = clipboard_button(clipboard_text: project_ref)
+ = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left")
.cross-project-reference.hide-collapsed
%span
Reference:
%cite{ title: project_ref }
= project_ref
- = clipboard_button(clipboard_text: project_ref)
+ = 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/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 3200aacf542..9e6a76e1ddb 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -9,7 +9,7 @@
.pull-right.light #{milestone.percent_complete(current_user)}% complete
.row
.col-sm-6
- = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
+ = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
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/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index 9af9dae04f0..18b8daf4e1e 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -2,7 +2,7 @@ class ReactiveCachingWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
- def perform(class_name, id)
+ def perform(class_name, id, *args)
klass = begin
Kernel.const_get(class_name)
rescue NameError
@@ -10,6 +10,6 @@ class ReactiveCachingWorker
end
return unless klass
- klass.find_by(id: id).try(:exclusively_update_reactive_cache!)
+ klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args)
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/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/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/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/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/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/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/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/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/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-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/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/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/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/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/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/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/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/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/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/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/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-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-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-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-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/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_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_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/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/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/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/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/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/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/config/application.rb b/config/application.rb
index 1de7fb7bdb8..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"
@@ -106,11 +108,11 @@ module Gitlab
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "terminal/terminal_bundle.js"
+ config.assets.precompile << "filtered_search/filtered_search_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
config.assets.precompile << "vue_pipelines_index/index.js"
- config.assets.precompile << "vue_pagination/index.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
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/routes/project.rb b/config/routes/project.rb
index 26e2dc9e6e7..6620b765e02 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -94,6 +94,7 @@ constraints(ProjectUrlConstrainer.new) do
get :pipelines
get :merge_check
post :merge
+ get :merge_widget_refresh
post :cancel_merge_when_build_succeeds
get :ci_status
get :ci_environments_status
@@ -306,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/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb
index 696914f8e4d..29040583aa2 100644
--- a/db/migrate/20161117114805_remove_undeleted_groups.rb
+++ b/db/migrate/20161117114805_remove_undeleted_groups.rb
@@ -5,47 +5,87 @@ class RemoveUndeletedGroups < ActiveRecord::Migration
DOWNTIME = false
def up
+ is_ee = defined?(Gitlab::License)
+
+ if is_ee
+ execute <<-EOF.strip_heredoc
+ DELETE FROM path_locks
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ );
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM remote_mirrors
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ );
+ EOF
+ end
+
execute <<-EOF.strip_heredoc
- DELETE FROM projects
- WHERE namespace_id IN (
- SELECT id FROM (
- SELECT id
- FROM namespaces
- WHERE deleted_at IS NOT NULL
- ) namespace_ids
+ DELETE FROM lists
+ WHERE label_id IN (
+ SELECT id
+ FROM labels
+ WHERE group_id IN (#{namespaces_pending_removal})
+ );
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM lists
+ WHERE board_id IN (
+ SELECT id
+ FROM boards
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ )
);
EOF
- if defined?(Gitlab::License)
+ execute <<-EOF.strip_heredoc
+ DELETE FROM labels
+ WHERE group_id IN (#{namespaces_pending_removal});
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM boards
+ WHERE project_id IN (
+ SELECT project_id
+ FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal})
+ )
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ DELETE FROM projects
+ WHERE namespace_id IN (#{namespaces_pending_removal});
+ EOF
+
+ if is_ee
# EE adds these columns but we have to make sure this data is cleaned up
# here before we run the DELETE below. An alternative would be patching
# this migration in EE but this will only result in a mess and confusing
# migrations.
execute <<-EOF.strip_heredoc
DELETE FROM protected_branch_push_access_levels
- WHERE group_id IN (
- SELECT id FROM (
- SELECT id
- FROM namespaces
- WHERE deleted_at IS NOT NULL
- ) namespace_ids
- );
+ WHERE group_id IN (#{namespaces_pending_removal});
EOF
execute <<-EOF.strip_heredoc
DELETE FROM protected_branch_merge_access_levels
- WHERE group_id IN (
- SELECT id FROM (
- SELECT id
- FROM namespaces
- WHERE deleted_at IS NOT NULL
- ) namespace_ids
- );
+ WHERE group_id IN (#{namespaces_pending_removal});
EOF
end
- # This removes namespaces that were supposed to be soft deleted but still
- # reside in the database.
+ # This removes namespaces that were supposed to be deleted but still reside
+ # in the database.
execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;"
end
@@ -54,4 +94,12 @@ class RemoveUndeletedGroups < ActiveRecord::Migration
# If someone is trying to rollback for other reasons, we should not throw an Exception.
# raise ActiveRecord::IrreversibleMigration
end
+
+ def namespaces_pending_removal
+ "SELECT id FROM (
+ SELECT id
+ FROM namespaces
+ WHERE deleted_at IS NOT NULL
+ ) namespace_ids"
+ end
end
diff --git a/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb
new file mode 100644
index 00000000000..b8d8742ae40
--- /dev/null
+++ b/db/migrate/20161201001911_add_plant_uml_url_to_application_settings.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPlantUmlUrlToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :plantuml_url, :string
+ end
+end
diff --git a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..3677f978cc2
--- /dev/null
+++ b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :plantuml_enabled, :boolean
+ end
+end
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..2cbe626d752
--- /dev/null
+++ b/db/migrate/20161223034433_add_estimate_to_issuables_ce.rb
@@ -0,0 +1,15 @@
+class AddEstimateToIssuablesCe < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ 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
+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..e8a4b406012
--- /dev/null
+++ b/db/migrate/20161223034646_create_timelogs_ce.rb
@@ -0,0 +1,20 @@
+class CreateTimelogsCe < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ 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
+end
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index 809b09feb84..a0ce927161f 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -14,17 +14,25 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
namespace_id = user['namespace_id']
path_was = user['username']
path_was_wildcard = quote_string("#{path_was}/%")
- path = quote_string(rename_path(path_was))
+ path = quote_string(new_path(path_was))
move_namespace(namespace_id, path_was, path)
- execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}"
- execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}"
- execute "UPDATE users SET username = '#{path}' WHERE id = #{id}"
-
- select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
- new_path = "#{path}/#{route['path'].split('/').last}"
- execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
+ begin
+ execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}"
+ execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}"
+ execute "UPDATE users SET username = '#{path}' WHERE id = #{id}"
+
+ select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
+ new_path = "#{path}/#{route['path'].split('/').last}"
+ execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
+ end
+ rescue => e
+ say("Couldn't update routes for path #{path_was} to #{path}")
+ # Move namespace back
+ move_namespace(namespace_id, path, path_was)
+
+ raise e
end
end
end
@@ -45,19 +53,30 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end
+ def path_exists?(path, repository_storage_path)
+ repository_storage_path && gitlab_shell.exists?(repository_storage_path, path)
+ end
+
# Accepts invalid path like test.git and returns test_git or
# test_git1 if test_git already taken
- def rename_path(path)
+ def new_path(path)
# To stay closer with original name and reduce risk of duplicates
# we rename suffix instead of removing it
path = path.sub(/\.git\z/, '_git')
- counter = 0
- base = path
+ check_routes(path.dup, 0, path)
+ end
+
+ def check_routes(base, counter, path)
+ route_exists = route_exists?(path)
- while route_exists?(path)
- counter += 1
- path = "#{base}#{counter}"
+ Gitlab.config.repositories.storages.each_value do |storage|
+ if route_exists || path_exists?(path, storage)
+ counter += 1
+ path = "#{base}#{counter}"
+
+ return check_routes(base, counter, path)
+ end
end
path
@@ -82,6 +101,14 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
end
end
- Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ begin
+ Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ rescue => e
+ if path.nil?
+ say("Couldn't find a storage path for #{namespace_id}, #{path_was} -- skipping")
+ else
+ raise e
+ end
+ end
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/20170106142508_fill_authorized_projects.rb b/db/post_migrate/20170106142508_fill_authorized_projects.rb
new file mode 100644
index 00000000000..314c8440c8b
--- /dev/null
+++ b/db/post_migrate/20170106142508_fill_authorized_projects.rb
@@ -0,0 +1,30 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FillAuthorizedProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # We're not inserting any data so we don't need to start a transaction.
+ disable_ddl_transaction!
+
+ def up
+ relation = User.select(:id).
+ where('authorized_projects_populated IS NOT TRUE')
+
+ relation.find_in_batches(batch_size: 1_000) do |rows|
+ args = rows.map { |row| [row.id] }
+
+ Sidekiq::Client.push_bulk('class' => 'AuthorizedProjectsWorker', 'args' => args)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f3bf7ced393..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"
@@ -107,6 +107,8 @@ ActiveRecord::Schema.define(version: 20170106172224) do
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
t.boolean "html_emails_enabled", default: true
+ t.string "plantuml_url"
+ t.boolean "plantuml_enabled"
end
create_table "audit_events", force: :cascade do |t|
@@ -222,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
@@ -325,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
@@ -504,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
@@ -683,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
@@ -1126,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..e329131b8ee 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,5 +1,9 @@
# 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.
@@ -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/img/integration/plantuml-example.png b/doc/administration/img/integration/plantuml-example.png
new file mode 100644
index 00000000000..cb64eca1a8a
--- /dev/null
+++ b/doc/administration/img/integration/plantuml-example.png
Binary files differ
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
new file mode 100644
index 00000000000..e5cf592e0a6
--- /dev/null
+++ b/doc/administration/integration/plantuml.md
@@ -0,0 +1,87 @@
+# PlantUML & GitLab
+
+> [Introduced][ce-7810] in GitLab 8.16.
+
+When [PlantUML](http://plantuml.com) integration is enabled and configured in
+GitLab we are able to create simple diagrams in AsciiDoc documents created in
+snippets, wikis, and repos.
+
+## PlantUML Server
+
+Before you can enable PlantUML in GitLab; you need to set up your own PlantUML
+server that will generate the diagrams. Installing and configuring your
+own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat.
+
+First you need to create a `plantuml.war` file from the source code:
+
+```
+sudo apt-get install graphviz openjdk-7-jdk git-core maven
+git clone https://github.com/plantuml/plantuml-server.git
+cd plantuml-server
+mvn package
+```
+
+The above sequence of commands will generate a WAR file that can be deployed
+using Tomcat:
+
+```
+sudo apt-get install tomcat7
+sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war
+sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war
+sudo service restart tomcat7
+```
+
+Once the Tomcat service restarts the PlantUML service will be ready and
+listening for requests on port 8080:
+
+```
+http://localhost:8080/plantuml
+```
+
+you can change these defaults by editing the `/etc/tomcat7/server.xml` file.
+
+
+## GitLab
+
+You need to enable PlantUML integration from Settings under Admin Area. To do
+that, login with an Admin account and do following:
+
+ - in GitLab go to **Admin Area** and then **Settings**
+ - scroll to bottom of the page until PlantUML section
+ - check **Enable PlantUML** checkbox
+ - set the PlantUML instance as **PlantUML URL**
+
+## Creating Diagrams
+
+With PlantUML integration enabled and configured, we can start adding diagrams to
+our AsciiDoc snippets, wikis and repos using blocks:
+
+```
+[plantuml, format="png", id="myDiagram", width="200px"]
+--
+Bob->Alice : hello
+Alice -> Bob : Go Away
+--
+```
+
+The above block will be converted to an HTML img tag with source pointing to the
+PlantUML instance. If the PlantUML server is correctly configured, this should
+render a nice diagram instead of the block:
+
+![PlantUML Integration](../img/integration/plantuml-example.png)
+
+Inside the block you can add any of the supported diagrams by PlantUML such as
+[Sequence](http://plantuml.com/sequence-diagram), [Use Case](http://plantuml.com/use-case-diagram),
+[Class](http://plantuml.com/class-diagram), [Activity](http://plantuml.com/activity-diagram-legacy),
+[Component](http://plantuml.com/component-diagram), [State](http://plantuml.com/state-diagram),
+and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML
+diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block.
+
+Some parameters can be added to the block definition:
+
+ - *format*: Can be either `png` or `svg`. Note that `svg` is not supported by
+ all browsers so use with care. The default is `png`.
+ - *id*: A CSS id added to the diagram HTML tag.
+ - *width*: Width attribute added to the img tag.
+ - *height*: Height attribute added to the img tag.
+
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/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 119125bcd3d..b276d1ad918 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -23,12 +23,15 @@ GET /issues?state=closed
GET /issues?labels=foo
GET /issues?labels=foo,bar
GET /issues?labels=foo,bar&state=opened
+GET /issues?milestone=1.0.0
+GET /issues?milestone=1.0.0&state=opened
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned |
+| `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
@@ -709,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/api/settings.md b/doc/api/settings.md
index 0bd38a6e664..f86c7cc2f94 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -44,7 +44,9 @@ Example response:
"repository_storage": "default",
"repository_storages": ["default"],
"koding_enabled": false,
- "koding_url": null
+ "koding_url": null,
+ "plantuml_enabled": false,
+ "plantuml_url": null
}
```
@@ -80,6 +82,8 @@ PUT /application/settings
| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources |
+| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
+| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
@@ -112,6 +116,8 @@ Example response:
"container_registry_token_expire_delay": 5,
"repository_storage": "default",
"koding_enabled": false,
- "koding_url": null
+ "koding_url": null,
+ "plantuml_enabled": false,
+ "plantuml_url": null
}
```
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/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/install/installation.md b/doc/install/installation.md
index 9dba03b1924..9cebed34b7e 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -601,6 +601,12 @@ If you want to connect the Redis server via socket, then use the "unix:" URL sch
production:
url: unix:/path/to/redis/socket
+Also you can use environment variables in the `config/resque.yml` file:
+
+ # example
+ production:
+ url: <%= ENV.fetch('GITLAB_REDIS_URL') %>
+
### Custom SSH Connection
If you are running SSH on a non-standard port, you must change the GitLab user's SSH config.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index ed843c0bfa9..e97430feb57 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -16,6 +16,7 @@ See the documentation below for details on how to configure these services.
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
+- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents.
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
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/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/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md
index b6b5c741d90..d9ff573d185 100644
--- a/doc/project_services/slack_slash_commands.md
+++ b/doc/project_services/slack_slash_commands.md
@@ -6,7 +6,7 @@ Slack commands give users an extra interface to perform common operations
from the chat environment. This allows one to, for example, create an issue as
soon as the idea was discussed in chat.
For all available commands try the help subcommand, for example: `/gitlab help`,
-all review the [full list of commands](../integrations/chat_commands.md).
+all review the [full list of commands](../integration/chat_commands.md).
## Prerequisites
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/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 51f7454610d..f6b4db71b44 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -94,7 +94,7 @@ In the example below we use Amazon S3 for storage, but Fog also lets you use
for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
-For omnibus packages:
+For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['backup_upload_connection'] = {
@@ -109,6 +109,8 @@ gitlab_rails['backup_upload_connection'] = {
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
+Make sure to run `sudo gitlab-ctl reconfigure` after editing `/etc/gitlab/gitlab.rb` to reflect the changes.
+
For installations from source:
```yaml
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index 5f6a6c6503e..a6546cffce2 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -14,6 +14,7 @@ do.
|:---------------------------|:-------------|
| `/close` | Close the issue or merge request |
| `/reopen` | Reopen the issue or merge request |
+| `/merge` | Merge (when build succeeds) |
| `/title <New title>` | Change title |
| `/assign @username` | Assign |
| `/unassign` | Remove assignee |
@@ -29,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..b317bd79ded 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -19,6 +19,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/admin/groups.feature b/features/admin/groups.feature
deleted file mode 100644
index 657e847cf4a..00000000000
--- a/features/admin/groups.feature
+++ /dev/null
@@ -1,49 +0,0 @@
-@admin
-Feature: Admin Groups
- Background:
- Given I sign in as an admin
- And I have group with projects
- And User "John Doe" exists
- And I visit admin groups page
-
- Scenario: See group list
- Then I should be all groups
-
- Scenario: Create a group
- When I click new group link
- And submit form with new group info
- Then I should be redirected to group page
- And I should see newly created group
-
- @javascript
- Scenario: Add user into projects in group
- When I visit admin group page
- When I select user "John Doe" from user list as "Reporter"
- Then I should see "John Doe" in team list in every project as "Reporter"
-
- Scenario: Shared projects
- Given group has shared projects
- When I visit group page
- Then I should see project shared with group
-
- @javascript
- Scenario: Invite user to a group by e-mail
- When I visit admin group page
- When I select user "johndoe@gitlab.com" from user list as "Reporter"
- Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter"
-
- @javascript
- Scenario: Signed in admin should be able to add himself to a group
- Given "John Doe" is owner of group "Owned"
- When I visit group "Owned" members page
- When I select current user as "Developer"
- Then I should see current user as "Developer"
-
- @javascript
- Scenario: Signed in admin should be able to remove himself from group
- Given current user is developer of group "Owned"
- When I visit group "Owned" members page
- Then I should see current user as "Developer"
- When I click on the "Remove User From Group" button for current user
- When I visit group "Owned" members page
- Then I should not see current user as "Developer"
diff --git a/features/admin/users.feature b/features/admin/users.feature
deleted file mode 100644
index 6755645778a..00000000000
--- a/features/admin/users.feature
+++ /dev/null
@@ -1,65 +0,0 @@
-@admin
-Feature: Admin Users
- Background:
- Given I sign in as an admin
- And system has users
-
- Scenario: On Admin Users
- Given I visit admin users page
- Then I should see all users
-
- Scenario: Edit user and change username to non ascii char
- When I visit admin users page
- And Click edit
- And Input non ascii char in username
- And Click save
- Then See username error message
- And Not changed form action url
-
- Scenario: Show user attributes
- Given user "Mike" with groups and projects
- Given I visit admin users page
- And click on "Mike" link
- Then I should see user "Mike" details
-
- Scenario: Edit my user attributes
- Given I visit admin users page
- And click edit on my user
- When I submit modified user
- Then I see user attributes changed
-
- @javascript
- Scenario: Remove users secondary email
- Given I visit admin users page
- And I view the user with secondary email
- And I see the secondary email
- When I click remove secondary email
- Then I should not see secondary email anymore
-
- Scenario: Show user keys
- Given user "Pete" with ssh keys
- And I visit admin users page
- And click on user "Pete"
- And click on ssh keys tab
- Then I should see key list
- And I click on the key title
- Then I should see key details
- And I click on remove key
- Then I should see the key removed
-
- Scenario: Show user identities
- Given user "Pete" with twitter account
- And I visit "Pete" identities page in admin
- Then I should see twitter details
-
- Scenario: Update user identities
- Given user "Pete" with twitter account
- And I visit "Pete" identities page in admin
- And I modify twitter identity
- Then I should see twitter details updated
-
- Scenario: Remove user identities
- Given user "Pete" with twitter account
- And I visit "Pete" identities page in admin
- And I remove twitter identity
- Then I should not see twitter details
diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature
deleted file mode 100644
index bd883a0ebfa..00000000000
--- a/features/dashboard/active_tab.feature
+++ /dev/null
@@ -1,24 +0,0 @@
-@dashboard
-Feature: Dashboard Active Tab
- Background:
- Given I sign in as a user
-
- Scenario: On Dashboard Home
- Given I visit dashboard page
- Then the active main tab should be Home
- And no other main tabs should be active
-
- Scenario: On Dashboard Issues
- Given I visit dashboard issues page
- Then the active main tab should be Issues
- And no other main tabs should be active
-
- Scenario: On Dashboard Merge Requests
- Given I visit dashboard merge requests page
- Then the active main tab should be Merge Requests
- And no other main tabs should be active
-
- Scenario: On Dashboard Groups
- Given I visit dashboard groups page
- Then the active main tab should be Groups
- And no other main tabs should be active
diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature
deleted file mode 100644
index bed9282f1c6..00000000000
--- a/features/dashboard/archived_projects.feature
+++ /dev/null
@@ -1,17 +0,0 @@
-@dashboard
-Feature: Dashboard Archived Projects
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I own project "Forum"
- And project "Forum" is archived
- And I visit dashboard page
-
- Scenario: I should see non-archived projects on dashboard
- Then I should see "Shop" project link
- And I should not see "Forum" project link
-
- Scenario: I toggle show of archived projects on dashboard
- When I click "Show archived projects" link
- Then I should see "Shop" project link
- And I should see "Forum" project link
diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature
deleted file mode 100644
index 3ae2c679dc1..00000000000
--- a/features/dashboard/group.feature
+++ /dev/null
@@ -1,13 +0,0 @@
-@dashboard
-Feature: Dashboard Group
- Background:
- Given I sign in as "John Doe"
- And "John Doe" is owner of group "Owned"
- And "John Doe" is guest of group "Guest"
-
- Scenario: Create a group from dasboard
- And I visit dashboard groups page
- And I click new group link
- And submit form with new group "Samurai" info
- Then I should be redirected to group "Samurai" page
- And I should see newly created group "Samurai"
diff --git a/features/dashboard/help.feature b/features/dashboard/help.feature
deleted file mode 100644
index bca2772897b..00000000000
--- a/features/dashboard/help.feature
+++ /dev/null
@@ -1,9 +0,0 @@
-@dashboard
-Feature: Dashboard Help
- Background:
- Given I sign in as a user
- And I visit the "Rake Tasks" help page
-
- Scenario: The markdown should be rendered correctly
- Then I should see "Rake Tasks" page markdown rendered
- And Header "Rebuild project satellites" should have correct ids and links
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/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature
deleted file mode 100644
index 49d7a3b9af2..00000000000
--- a/features/project/issues/filter_labels.feature
+++ /dev/null
@@ -1,28 +0,0 @@
-@project_issues
-Feature: Project Issues Filter Labels
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" has labels: "bug", "feature", "enhancement"
- And project "Shop" has issue "Bugfix1" with labels: "bug", "feature"
- And project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"
- And project "Shop" has issue "Feature1" with labels: "feature"
- Given I visit project "Shop" issues page
-
- @javascript
- Scenario: I filter by one label
- Given I click link "bug"
- And I click "dropdown close button"
- Then I should see "Bugfix1" in issues list
- And I should see "Bugfix2" in issues list
- And I should not see "Feature1" in issues list
-
- # TODO: make labels filter works according to this scanario
- # right now it looks for label 1 OR label 2. Old behaviour (this test) was
- # all issues that have both label 1 AND label 2
- #Scenario: I filter by two labels
- #Given I click link "bug"
- #And I click link "feature"
- #Then I should see "Bugfix1" in issues list
- #And I should not see "Bugfix2" in issues list
- #And I should not see "Feature1" in issues list
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 80670063ea0..b2b4fe72220 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -26,12 +26,6 @@ Feature: Project Issues
Given I click link "Release 0.4"
Then I should see issue "Release 0.4"
- @javascript
- Scenario: I filter by author
- Given I add a user to project "Shop"
- And I click "author" dropdown
- Then I see current user as the first user
-
Scenario: I submit new unassigned issue
Given I click link "New Issue"
And I submit new issue "500 error on profile"
@@ -84,56 +78,6 @@ Feature: Project Issues
And I sort the list by "Least popular"
Then The list should be sorted by "Least popular"
- @javascript
- Scenario: I search issue
- Given I fill in issue search with "Re"
- Then I should see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
- @javascript
- Scenario: I search issue that not exist
- Given I fill in issue search with "Bu"
- Then I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
-
- @javascript
- Scenario: I search all issues
- Given I click link "All"
- And I fill in issue search with ".3"
- Then I should see "Release 0.3" in issues
- And I should not see "Release 0.4" in issues
-
- @javascript
- Scenario: Search issues when search string exactly matches issue description
- Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
- And I fill in issue search with 'Description for issue1'
- Then I should see 'Bugfix1' in issues
- And I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
- @javascript
- Scenario: Search issues when search string partially matches issue description
- Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
- And project 'Shop' has issue 'Feature1' with description: 'Feature submitted for issue1'
- And I fill in issue search with 'issue1'
- Then I should see 'Feature1' in issues
- Then I should see 'Bugfix1' in issues
- And I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
- @javascript
- Scenario: Search issues when search string matches no issue description
- Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1'
- And I fill in issue search with 'Rock and roll'
- Then I should not see 'Bugfix1' in issues
- And I should not see "Release 0.4" in issues
- And I should not see "Release 0.3" in issues
- And I should not see "Tweet control" in issues
-
-
# Markdown
Scenario: Headers inside the description should have ids generated for them.
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
deleted file mode 100644
index 9396a76f0a2..00000000000
--- a/features/steps/admin/groups.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-class Spinach::Features::AdminGroups < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedGroup
- include SharedPaths
- include SharedUser
- include SharedActiveTab
- include Select2Helper
-
- When 'I visit admin group page' do
- visit admin_group_path(current_group)
- end
-
- When 'I click new group link' do
- click_link "New Group"
- end
-
- step 'I have group with projects' do
- @group = create(:group)
- @project = create(:project, group: @group)
- @event = create(:closed_issue_event, project: @project)
-
- @project.team << [current_user, :master]
- end
-
- step 'submit form with new group info' do
- fill_in 'group_path', with: 'gitlab'
- fill_in 'group_description', with: 'Group description'
- click_button "Create group"
- end
-
- step 'I should see newly created group' do
- expect(page).to have_content "Group: gitlab"
- expect(page).to have_content "Group description"
- end
-
- step 'I should be redirected to group page' do
- expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab'))
- end
-
- When 'I select user "John Doe" from user list as "Reporter"' do
- select2(user_john.id, from: "#user_ids", multiple: true)
- page.within "#new_project_member" do
- select "Reporter", from: "access_level"
- end
- click_button "Add users to group"
- end
-
- When 'I select user "johndoe@gitlab.com" from user list as "Reporter"' do
- select2('johndoe@gitlab.com', from: "#user_ids", multiple: true)
- page.within "#new_project_member" do
- select "Reporter", from: "access_level"
- end
- click_button "Add users to group"
- end
-
- step 'I should see "John Doe" in team list in every project as "Reporter"' do
- page.within ".group-users-list" do
- expect(page).to have_content "John Doe"
- expect(page).to have_content "Reporter"
- end
- end
-
- step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do
- page.within ".group-users-list" do
- expect(page).to have_content "johndoe@gitlab.com"
- expect(page).to have_content "Invited by"
- expect(page).to have_content "Reporter"
- end
- end
-
- step 'I should be all groups' do
- Group.all.each do |group|
- expect(page).to have_content group.name
- end
- end
-
- step 'group has shared projects' do
- share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
- share_link.group_id = current_group.id
- share_link.save!
- end
-
- step 'I visit group page' do
- visit admin_group_path(current_group)
- end
-
- step 'I should see project shared with group' do
- expect(page).to have_content(shared_project.name_with_namespace)
- expect(page).to have_content "Projects shared with"
- end
-
- step 'we have user "John Doe" in group' do
- current_group.add_reporter(user_john)
- end
-
- step 'I should not see "John Doe" in team list' do
- page.within ".group-users-list" do
- expect(page).not_to have_content "John Doe"
- end
- end
-
- step 'I select current user as "Developer"' do
- page.within ".users-group-form" do
- select2(current_user.id, from: "#user_ids", multiple: true)
- select "Developer", from: "access_level"
- end
-
- click_button "Add to group"
- end
-
- step 'I should see current user as "Developer"' do
- page.within '.content-list' do
- expect(page).to have_content(current_user.name)
- expect(page).to have_content('Developer')
- end
- end
-
- step 'I click on the "Remove User From Group" button for current user' do
- find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see current user as "Developer"' do
- page.within '.content-list' do
- expect(page).not_to have_content(current_user.name)
- expect(page).not_to have_content('Developer')
- end
- end
-
- protected
-
- def current_group
- @group ||= Group.first
- end
-
- def shared_project
- @shared_project ||= create(:empty_project)
- end
-
- def user_john
- @user_john ||= User.find_by(name: "John Doe")
- end
-end
diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb
deleted file mode 100644
index 8fb8a86d58b..00000000000
--- a/features/steps/admin/users.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-class Spinach::Features::AdminUsers < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedAdmin
-
- before do
- allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
- allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
- end
-
- after do
- allow(Gitlab::OAuth::Provider).to receive(:providers).and_call_original
- allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_call_original
- end
-
- step 'I should see all users' do
- User.all.each do |user|
- expect(page).to have_content user.name
- end
- end
-
- step 'Click edit' do
- @user = User.first
- find("#edit_user_#{@user.id}").click
- end
-
- step 'Input non ascii char in username' do
- fill_in 'user_username', with: "\u3042\u3044"
- end
-
- step 'Click save' do
- click_button("Save")
- end
-
- step 'See username error message' do
- page.within "#error_explanation" do
- expect(page).to have_content "Username"
- end
- end
-
- step 'Not changed form action url' do
- expect(page).to have_selector %(form[action="/admin/users/#{@user.username}"])
- end
-
- step 'I submit modified user' do
- check :user_can_create_group
- click_button 'Save'
- end
-
- step 'I see user attributes changed' do
- expect(page).to have_content 'Can create groups: Yes'
- end
-
- step 'click edit on my user' do
- find("#edit_user_#{current_user.id}").click
- end
-
- step 'I view the user with secondary email' do
- @user_with_secondary_email = User.last
- @user_with_secondary_email.emails.new(email: "secondary@example.com")
- @user_with_secondary_email.save
- visit "/admin/users/#{@user_with_secondary_email.username}"
- end
-
- step 'I see the secondary email' do
- expect(page).to have_content "Secondary email: #{@user_with_secondary_email.emails.last.email}"
- end
-
- step 'I click remove secondary email' do
- find("#remove_email_#{@user_with_secondary_email.emails.last.id}").click
- end
-
- step 'I should not see secondary email anymore' do
- expect(page).not_to have_content "Secondary email:"
- end
-
- step 'user "Mike" with groups and projects' do
- user = create(:user, name: 'Mike')
-
- project = create(:empty_project)
- project.team << [user, :developer]
-
- group = create(:group)
- group.add_developer(user)
- end
-
- step 'click on "Mike" link' do
- click_link "Mike"
- end
-
- step 'I should see user "Mike" details' do
- expect(page).to have_content 'Account'
- expect(page).to have_content 'Personal projects limit'
- end
-
- step 'user "Pete" with ssh keys' do
- user = create(:user, name: 'Pete')
- create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1")
- create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2")
- end
-
- step 'click on user "Pete"' do
- click_link 'Pete'
- end
-
- step 'I should see key list' do
- expect(page).to have_content 'ssh-rsa Key2'
- expect(page).to have_content 'ssh-rsa Key1'
- end
-
- step 'I click on the key title' do
- click_link 'ssh-rsa Key2'
- end
-
- step 'I should see key details' do
- expect(page).to have_content 'ssh-rsa Key2'
- expect(page).to have_content 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2'
- end
-
- step 'I click on remove key' do
- click_link 'Remove'
- end
-
- step 'I should see the key removed' do
- expect(page).not_to have_content 'ssh-rsa Key2'
- end
-
- step 'user "Pete" with twitter account' do
- @user = create(:user, name: 'Pete')
- @user.identities.create!(extern_uid: '123456', provider: 'twitter')
- end
-
- step 'I visit "Pete" identities page in admin' do
- visit admin_user_identities_path(@user)
- end
-
- step 'I should see twitter details' do
- expect(page).to have_content 'Pete'
- expect(page).to have_content 'twitter'
- end
-
- step 'I modify twitter identity' do
- find('.table').find(:link, 'Edit').click
- fill_in 'identity_extern_uid', with: '654321'
- select 'twitter_updated', from: 'identity_provider'
- click_button 'Save changes'
- end
-
- step 'I should see twitter details updated' do
- expect(page).to have_content 'Pete'
- expect(page).to have_content 'twitter_updated'
- expect(page).to have_content '654321'
- end
-
- step 'I remove twitter identity' do
- click_link 'Delete'
- end
-
- step 'I should not see twitter details' do
- expect(page).to have_content 'Pete'
- expect(page).not_to have_content 'twitter'
- end
-
- step 'click on ssh keys tab' do
- click_link 'SSH keys'
- end
-end
diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb
deleted file mode 100644
index 04fe96cef22..00000000000
--- a/features/steps/dashboard/active_tab.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSidebarActiveTab
-end
diff --git a/features/steps/dashboard/archived_projects.rb b/features/steps/dashboard/archived_projects.rb
deleted file mode 100644
index 6510f8d9b32..00000000000
--- a/features/steps/dashboard/archived_projects.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-class Spinach::Features::DashboardArchivedProjects < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- When 'project "Forum" is archived' do
- project = Project.find_by(name: "Forum")
- project.update_attribute(:archived, true)
- end
-
- step 'I should see "Shop" project link' do
- expect(page).to have_link "Shop"
- end
-
- step 'I should not see "Forum" project link' do
- expect(page).not_to have_link "Forum"
- end
-
- step 'I should see "Forum" project link' do
- expect(page).to have_link "Forum"
- end
-
- step 'I click "Show archived projects" link' do
- click_link "Show archived projects"
- end
-end
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/group.rb b/features/steps/dashboard/group.rb
deleted file mode 100644
index cf679fea530..00000000000
--- a/features/steps/dashboard/group.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedGroup
- include SharedPaths
- include SharedUser
-
- step 'I click new group link' do
- click_link "New Group"
- end
-
- step 'submit form with new group "Samurai" info' do
- fill_in 'group_path', with: 'Samurai'
- fill_in 'group_description', with: 'Tokugawa Shogunate'
- click_button "Create group"
- end
-
- step 'I should be redirected to group "Samurai" page' do
- expect(current_path).to eq group_path(Group.find_by(name: 'Samurai'))
- end
-
- step 'I should see newly created group "Samurai"' do
- expect(page).to have_content "Samurai"
- expect(page).to have_content "Tokugawa Shogunate"
- end
-end
diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb
deleted file mode 100644
index 3c5bf44c538..00000000000
--- a/features/steps/dashboard/help.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class Spinach::Features::DashboardHelp < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedMarkdown
-
- step 'I visit the help page' do
- visit help_path
- end
-
- step 'I visit the "Rake Tasks" help page' do
- visit help_page_path("administration/raketasks/maintenance")
- end
-
- step 'I should see "Rake Tasks" page markdown rendered' do
- expect(page).to have_content "Gather information about GitLab and the system it runs on"
- end
-
- step 'Header "Rebuild project satellites" should have correct ids and links' do
- header_should_have_correct_id_and_link(2, 'Check GitLab configuration', 'check-gitlab-configuration', '.documentation')
- end
-end
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 15b81fa529b..718cf924729 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -191,10 +191,6 @@ module SharedPaths
visit admin_background_jobs_path
end
- step 'I visit admin groups page' do
- visit admin_groups_path
- end
-
step 'I visit admin teams page' do
visit admin_teams_path
end
@@ -260,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/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/entities.rb b/lib/api/entities.rb
index d2fadf6a3d0..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
@@ -565,6 +572,8 @@ module API
expose :repository_storages
expose :koding_enabled
expose :koding_url
+ expose :plantuml_enabled
+ expose :plantuml_url
end
class Release < Grape::Entity
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 20b5bc1502a..49c5f0652ab 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -86,6 +86,10 @@ 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 authenticate!
unauthorized! unless current_user
end
@@ -222,7 +226,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)
@@ -294,7 +298,7 @@ module API
header['X-Sendfile'] = path
body
else
- file FileStreamer.new(path)
+ path
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 54b97402426..fe016c1ec0a 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -5,13 +5,31 @@ module API
before { authenticate! }
helpers do
- # TODO: Remove in 9.0 and switch to IssueFinder-based label filtering
- def filter_issues_labels(issues, labels)
- issues.includes(:labels).where('labels.title' => labels.split(','))
+ def find_issues(args = {})
+ args = params.merge(args)
+
+ args.delete(:id)
+ args[:milestone_title] = args.delete(:milestone)
+
+ match_all_labels = args.delete(:match_all_labels)
+ labels = args.delete(:labels)
+ args[:label_name] = labels if match_all_labels
+
+ args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
+
+ issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
+
+ # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
+ if !match_all_labels && labels.present?
+ issues = issues.includes(:labels).where('labels.title' => labels.split(','))
+ end
+
+ issues.reorder(args[:order_by] => args[:sort])
end
params :issues_params do
optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :milestone, type: String, desc: 'Milestone title'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
@@ -40,9 +58,7 @@ module API
use :issues_params
end
get do
- issues = IssuesFinder.new(current_user, scope: 'all', author_id: current_user.id, state: params[:state]).execute.inc_notes_with_associations
- issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = issues.reorder(params[:order_by] => params[:sort])
+ issues = find_issues(scope: 'authored')
present paginate(issues), with: Entities::Issue, current_user: current_user
end
@@ -61,15 +77,10 @@ module API
use :issues_params
end
get ":id/issues" do
- group = find_group!(params.delete(:id))
-
- params[:group_id] = group.id
- params[:milestone_title] = params.delete(:milestone)
- params[:label_name] = params.delete(:labels)
+ group = find_group!(params[:id])
- issues = IssuesFinder.new(current_user, params).execute
+ issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
- issues = issues.reorder(params[:order_by] => params[:sort])
present paginate(issues), with: Entities::Issue, current_user: current_user
end
end
@@ -78,23 +89,21 @@ 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
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
- optional :iid, type: Integer, desc: 'The IID of the issue'
+ optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
use :issues_params
end
get ":id/issues" do
- issues = IssuesFinder.new(current_user,
- project_id: user_project.id,
- state: params[:state],
- milestone_title: params[:milestone]).execute.inc_notes_with_associations
- issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
- issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
- issues = issues.reorder(params[:order_by] => params[:sort])
+ project = find_project(params[:id])
+
+ issues = find_issues(project_id: project.id)
present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 5d1fe22f2df..e77af4b7a0d 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,7 +118,7 @@ module API
success Entities::MergeRequest
end
get path do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
@@ -125,7 +127,7 @@ module API
success Entities::RepoCommit
end
get "#{path}/commits" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.commits, with: Entities::RepoCommit
end
@@ -134,7 +136,7 @@ module API
success Entities::MergeRequestChanges
end
get "#{path}/changes" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
@@ -153,7 +155,7 @@ module API
:remove_source_branch
end
put path do
- merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
+ merge_request = find_project_merge_request(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
mr_params = declared_params(include_missing: false)
@@ -180,7 +182,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 +218,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,7 +235,7 @@ module API
use :pagination
end
get "#{path}/comments" do
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
@@ -248,7 +250,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])
+ merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :create_note, merge_request
opts = {
@@ -273,7 +275,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_project_merge_request(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/projects.rb b/lib/api/projects.rb
index 3be14e8eb76..941f47114a4 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -159,7 +159,7 @@ module API
use :sort_params
use :pagination
end
- get "/search/:query" do
+ get "/search/:query", requirements: { query: /[^\/]+/ } do
search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
projects = search_service.objects('projects', params[:page])
projects = projects.reorder(params[:order_by] => params[:sort])
@@ -295,13 +295,13 @@ module API
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
- ::Projects::UpdateService.new(user_project, current_user, attrs).execute
+ result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
- if user_project.errors.any?
- render_validation_error!(user_project)
- else
+ if result[:status] == :success
present user_project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project)
+ else
+ render_validation_error!(user_project)
end
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 9eb9a105bde..c5eff16a5de 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -93,6 +93,10 @@ module API
given koding_enabled: ->(val) { val } do
requires :koding_url, type: String, desc: 'The Koding team URL'
end
+ optional :plantuml_enabled, type: Boolean, desc: 'Enable PlantUML'
+ given plantuml_enabled: ->(val) { val } do
+ requires :plantuml_url, type: String, desc: 'The PlantUML server URL'
+ end
optional :version_check_enabled, type: Boolean, desc: 'Let GitLab inform you when an update is available.'
optional :email_author_in_body, type: Boolean, desc: 'Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead.'
optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
@@ -114,7 +118,7 @@ module API
:shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay,
:metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
:akismet_enabled, :admin_notification_email, :sentry_enabled,
- :repository_storage, :repository_checks_enabled, :koding_enabled,
+ :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
:version_check_enabled, :email_author_in_body, :html_emails_enabled,
:housekeeping_enabled
end
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/users.rb b/lib/api/users.rb
index 0db76ec7877..11a7368b4c0 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -93,7 +93,7 @@ module API
# Filter out params which are used later
user_params = declared_params(include_missing: false)
identity_attrs = user_params.slice(:provider, :extern_uid)
- confirm = params.delete(:confirm)
+ confirm = user_params.delete(:confirm)
user = User.new(user_params.except(:extern_uid, :provider))
user.skip_confirmation! unless confirm
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 84bfeac8041..ab7af1cad21 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)
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/asciidoc.rb b/lib/gitlab/asciidoc.rb
index fa234284361..0618107e2c3 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,5 +1,6 @@
require 'asciidoctor'
require 'asciidoctor/converter/html5'
+require "asciidoctor-plantuml"
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
@@ -29,6 +30,8 @@ module Gitlab
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
+ plantuml_setup
+
html = ::Asciidoctor.convert(input, asciidoc_opts)
html = Banzai.post_process(html, context)
@@ -36,6 +39,15 @@ module Gitlab
html.html_safe
end
+ def self.plantuml_setup
+ Asciidoctor::PlantUml.configure do |conf|
+ conf.url = ApplicationSetting.current.plantuml_url
+ conf.svg_enable = ApplicationSetting.current.plantuml_enabled
+ conf.png_enable = ApplicationSetting.current.plantuml_enabled
+ conf.txt_enable = false
+ end
+ end
+
class Html5Converter < Asciidoctor::Converter::Html5Converter
extend Asciidoctor::Converter::Config
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 9c391fa92a3..273118135a9 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -30,9 +30,9 @@ module Gitlab
return unless @branch_name
return unless project.protected_branch?(@branch_name)
- if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches)
+ if forced_push?
return "You are not allowed to force push code to a protected branch on this project."
- elsif Gitlab::Git.blank_ref?(@newrev) && user_access.cannot_do_action?(:remove_protected_branches)
+ elsif Gitlab::Git.blank_ref?(@newrev)
return "You are not allowed to delete protected branches from this project."
end
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index b7b4b91eb51..f7c530c7d9f 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -33,7 +33,6 @@ module Gitlab
validates :url,
length: { maximum: 255 },
- addressable_url: true,
allow_nil: true
validates :action,
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/current_settings.rb b/lib/gitlab/current_settings.rb
index 9d142f1b82e..2ff27e46d64 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -35,6 +35,7 @@ module Gitlab
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,
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/git/blame.rb b/lib/gitlab/git/blame.rb
index 46f3969b6e1..58193391926 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -1,5 +1,3 @@
-require_relative 'encoding_helper'
-
module Gitlab
module Git
class Blame
@@ -27,7 +25,7 @@ module Gitlab
private
def load_blame
- cmd = %W(git --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path})
# Read in binary mode to ensure ASCII-8BIT
raw_output = IO.popen(cmd, 'rb') {|io| io.read }
output = encode_utf8(raw_output)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 4a623311c14..b742d9e1e4b 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -1,6 +1,3 @@
-require_relative 'encoding_helper'
-require_relative 'path_helper'
-
module Gitlab
module Git
class Blob
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 963b326a730..7068e68a855 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1,6 +1,4 @@
# Gitlab::Git::Repository is a wrapper around native Rugged::Repository object
-require_relative 'encoding_helper'
-require_relative 'path_helper'
require 'forwardable'
require 'tempfile'
require 'forwardable'
@@ -332,7 +330,7 @@ module Gitlab
end
def log_by_shell(sha, options)
- cmd = %W(git --git-dir=#{path} log)
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
cmd += %W(-n #{options[:limit].to_i})
cmd += %w(--format=%H)
cmd += %W(--skip=#{options[:offset].to_i})
@@ -913,7 +911,7 @@ module Gitlab
return []
end
- cmd = %W(git --git-dir=#{path} ls-tree)
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree)
cmd += %w(-r)
cmd += %w(--full-tree)
cmd += %w(--full-name)
@@ -1108,7 +1106,7 @@ module Gitlab
end
def archive_to_file(treeish = 'master', filename = 'archive.tar.gz', format = nil, compress_cmd = %w(gzip -n))
- git_archive_cmd = %W(git --git-dir=#{path} archive)
+ git_archive_cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} archive)
# Put files into a directory before archiving
prefix = "#{archive_name(treeish)}/"
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 4d4e04e9e35..b8a5ac907a4 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -13,6 +13,7 @@ module Gitlab
if current_user
gon.current_user_id = current_user.id
+ gon.current_username = current_user.username
end
end
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/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index d01d47a6a7a..47f88727fc8 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -71,10 +71,17 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
- # endpoint.route is nil in the case of a 405 response
- if endpoint.route
- path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
- trans.action = "Grape##{endpoint.route.request_method} #{path}"
+ begin
+ route = endpoint.route
+ rescue
+ # endpoint.route is calling env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info]
+ # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response
+ # so we're rescuing exceptions and bailing out
+ end
+
+ if route
+ path = endpoint_paths_cache[route.request_method][route.path]
+ trans.action = "Grape##{route.request_method} #{path}"
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 9226da2d6b1..9384102acec 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -42,7 +42,7 @@ module Gitlab
return @_raw_config if defined?(@_raw_config)
begin
- @_raw_config = File.read(CONFIG_FILE).freeze
+ @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze
rescue Errno::ENOENT
@_raw_config = false
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 9e0b0e5ea98..a3fa7c1331a 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -61,11 +61,11 @@ module Gitlab
end
def file_name_regex
- @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@]*\z/.freeze
+ @file_name_regex ||= /\A[[[:alnum:]]_\-\.\@\+]*\z/.freeze
end
def file_name_regex_message
- "can contain only letters, digits, '_', '-', '@' and '.'."
+ "can contain only letters, digits, '_', '-', '@', '+' and '.'."
end
def file_path_regex
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/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/tasks/dev.rake b/lib/tasks/dev.rake
index 6f27972c4e4..5e94fba97bf 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -7,9 +7,4 @@ namespace :dev do
Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke
end
-
- desc 'GitLab | Start/restart foreman and watch for changes'
- task :foreman => :environment do
- sh 'rerun --dir app,config,lib -- foreman start'
- end
end
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index f9834a4dae8..a67c1fe1f27 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -3,7 +3,7 @@ namespace :gitlab do
desc "GitLab | Git | Repack"
task repack: :environment do
- failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
if failures.empty?
puts "Done".color(:green)
else
@@ -13,17 +13,17 @@ namespace :gitlab do
desc "GitLab | Git | Run garbage collection on all repos"
task gc: :environment do
- failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting")
if failures.empty?
puts "Done".color(:green)
else
output_failures(failures)
end
end
-
+
desc "GitLab | Git | Prune all repos"
task prune: :environment do
- failures = perform_git_cmd(%W(git prune), "Git Prune")
+ failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune")
if failures.empty?
puts "Done".color(:green)
else
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index 4f76dad7286..b77a5bb62d1 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -44,7 +44,7 @@ namespace :gitlab do
),
Template.new(
"https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
- /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/
+ /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
)
]
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/scripts/notify_slack.sh b/scripts/notify_slack.sh
index 0a4239e132c..6b3bc563c7a 100755
--- a/scripts/notify_slack.sh
+++ b/scripts/notify_slack.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
# Sends Slack notification ERROR_MSG to CHANNEL
# An env. variable CI_SLACK_WEBHOOK_URL needs to be set.
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/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 288984cfba9..19fbc2f7748 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -12,7 +12,7 @@ describe Dashboard::TodosController do
end
context 'when using pagination' do
- let(:last_page) { user.todos.page().total_pages }
+ let(:last_page) { user.todos.page.total_pages }
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
before 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/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 2a411d78395..7ea3ea4f376 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1048,4 +1048,72 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'GET merge_widget_refresh' do
+ let(:params) do
+ {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ format: :raw
+ }
+ end
+
+ before do
+ project.team << [user, :developer]
+ xhr :get, :merge_widget_refresh, params
+ end
+
+ context 'when merge in progress' do
+ let(:merge_request) { create(:merge_request, source_project: project, in_progress_merge_commit_sha: 'sha') }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to :success' do
+ expect(assigns(:status)).to eq(:success)
+ expect(response).to render_template('merge')
+ end
+ end
+
+ context 'when merge request was merged already' do
+ let(:merge_request) { create(:merge_request, source_project: project, state: :merged) }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to :success' do
+ expect(assigns(:status)).to eq(:success)
+ expect(response).to render_template('merge')
+ end
+ end
+
+ context 'when waiting for build' do
+ let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to :merge_when_build_succeeds' do
+ expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ expect(response).to render_template('merge')
+ end
+ end
+
+ context 'when no special status for MR' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'sets status to nil' do
+ expect(assigns(:status)).to be_nil
+ expect(response).to render_template('merge')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 92e38b02615..9f6d4ec6537 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -14,6 +14,54 @@ describe Projects::NotesController do
}
end
+ describe 'POST create' do
+ let(:merge_request) { create(:merge_request) }
+ let(:request_params) do
+ {
+ note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' },
+ namespace_id: project.namespace,
+ project_id: project,
+ merge_request_diff_head_sha: 'sha'
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "returns status 302 for html" do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+
+ it "returns status 200 for json" do
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when merge_request_diff_head_sha present' do
+ before do
+ service_params = {
+ note: 'some note',
+ noteable_id: merge_request.id.to_s,
+ noteable_type: 'MergeRequest',
+ merge_request_diff_head_sha: 'sha'
+ }
+
+ expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true))
+ end
+
+ it "returns status 302 for html" do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+ end
+ end
+
describe 'POST toggle_award_emoji' do
before do
sign_in(user)
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/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 5ddcaa60dc6..d0a63aa9403 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -245,7 +245,7 @@ describe ProjectsController do
expect(project.repository.path).to include(new_path)
expect(assigns(:repository).path).to eq(project.repository.path)
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(302)
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f7fa834d7a2..992580a6b34 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -24,10 +24,18 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+ trait :archived do
+ archived true
+ end
+
trait :access_requestable 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_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 9c19db6b420..a871e370ba2 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -1,15 +1,39 @@
require 'spec_helper'
feature 'Admin Groups', feature: true do
+ include Select2Helper
+
let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:user) { create :user }
+ let!(:group) { create :group }
+ let!(:current_user) { login_as :admin }
before do
- login_as(:admin)
-
stub_application_setting(default_group_visibility: internal)
end
+ describe 'list' do
+ it 'renders groups' do
+ visit admin_groups_path
+
+ expect(page).to have_content(group.name)
+ end
+ end
+
describe 'create a group' do
+ it 'creates new group' do
+ visit admin_groups_path
+
+ click_link "New Group"
+ fill_in 'group_path', with: 'gitlab'
+ fill_in 'group_description', with: 'Group description'
+ click_button "Create group"
+
+ expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab'))
+ expect(page).to have_content('Group: gitlab')
+ expect(page).to have_content('Group description')
+ end
+
scenario 'shows the visibility level radio populated with the default value' do
visit new_admin_group_path
@@ -37,6 +61,91 @@ feature 'Admin Groups', feature: true do
end
end
+ describe 'add user into a group', js: true do
+ shared_context 'adds user into a group' do
+ it do
+ visit admin_group_path(group)
+
+ select2(user_selector, from: '#user_ids', multiple: true)
+ page.within '#new_project_member' do
+ select2(Gitlab::Access::REPORTER, from: '#access_level')
+ end
+ click_button "Add users to group"
+ page.within ".group-users-list" do
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('Reporter')
+ end
+ end
+ end
+
+ it_behaves_like 'adds user into a group' do
+ let(:user_selector) { user.id }
+ end
+
+ it_behaves_like 'adds user into a group' do
+ let(:user_selector) { user.email }
+ end
+ end
+
+ describe 'add admin himself to a group' do
+ before do
+ group.add_user(:user, Gitlab::Access::OWNER)
+ end
+
+ it 'adds admin a to a group as developer', js: true do
+ visit group_group_members_path(group)
+
+ page.within '.users-group-form' do
+ select2(current_user.id, from: '#user_ids', multiple: true)
+ select 'Developer', from: 'access_level'
+ end
+
+ click_button 'Add to group'
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+ end
+ end
+
+ describe 'admin remove himself from a group', js: true do
+ it 'removes admin from the group' do
+ group.add_user(current_user, Gitlab::Access::DEVELOPER)
+
+ visit group_group_members_path(group)
+
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+
+ find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+
+ visit group_group_members_path(group)
+
+ page.within '.content-list' do
+ expect(page).not_to have_content(current_user.name)
+ expect(page).not_to have_content('Developer')
+ end
+ end
+ end
+
+ describe 'shared projects' do
+ it 'renders shared project' do
+ empty_project = create(:empty_project)
+ empty_project.project_group_links.create!(
+ group_access: Gitlab::Access::MASTER,
+ group: group
+ )
+
+ visit admin_group_path(group)
+
+ expect(page).to have_content(empty_project.name_with_namespace)
+ expect(page).to have_content('Projects shared with')
+ end
+ end
+
def expect_selected_visibility(level)
selector = "#group_visibility_level_#{level}[checked=checked]"
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_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 55ffc6761f8..a586f8d3184 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -1,9 +1,13 @@
require 'spec_helper'
-describe "Admin::Users", feature: true do
+describe "Admin::Users", feature: true do
include WaitForAjax
- before { login_as :admin }
+ let!(:user) do
+ create(:omniauth_user, provider: 'twitter', extern_uid: '123456')
+ end
+
+ let!(:current_user) { login_as :admin }
describe "GET /admin/users" do
before do
@@ -15,8 +19,10 @@ describe "Admin::Users", feature: true do
end
it "has users list" do
- expect(page).to have_content(@user.email)
- expect(page).to have_content(@user.name)
+ expect(page).to have_content(current_user.email)
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content(user.email)
+ expect(page).to have_content(user.name)
end
describe 'Two-factor Authentication filters' do
@@ -40,8 +46,6 @@ describe "Admin::Users", feature: true do
end
it 'counts users who have not enabled 2FA' do
- create(:user)
-
visit admin_users_path
page.within('.filter-two-factor-disabled small') do
@@ -50,8 +54,6 @@ describe "Admin::Users", feature: true do
end
it 'filters by users who have not enabled 2FA' do
- user = create(:user)
-
visit admin_users_path
click_link '2FA Disabled'
@@ -110,10 +112,10 @@ describe "Admin::Users", feature: true do
describe "GET /admin/users/:id" do
it "has user info" do
visit admin_users_path
- click_link @user.name
+ click_link user.name
- expect(page).to have_content(@user.email)
- expect(page).to have_content(@user.name)
+ expect(page).to have_content(user.email)
+ expect(page).to have_content(user.name)
end
describe 'Impersonation' do
@@ -126,7 +128,7 @@ describe "Admin::Users", feature: true do
end
it 'does not show impersonate button for admin itself' do
- visit admin_user_path(@user)
+ visit admin_user_path(current_user)
expect(page).not_to have_content('Impersonate')
end
@@ -158,7 +160,7 @@ describe "Admin::Users", feature: true do
it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username)
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
@@ -171,15 +173,15 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication status' do
it 'shows when enabled' do
- @user.update_attribute(:otp_required_for_login, true)
+ user.update_attribute(:otp_required_for_login, true)
- visit admin_user_path(@user)
+ visit admin_user_path(user)
expect_two_factor_status('Enabled')
end
it 'shows when disabled' do
- visit admin_user_path(@user)
+ visit admin_user_path(user)
expect_two_factor_status('Disabled')
end
@@ -194,9 +196,8 @@ describe "Admin::Users", feature: true do
describe "GET /admin/users/:id/edit" do
before do
- @simple_user = create(:user)
visit admin_users_path
- click_link "edit_user_#{@simple_user.id}"
+ click_link "edit_user_#{user.id}"
end
it "has user edit page" do
@@ -214,45 +215,58 @@ describe "Admin::Users", feature: true do
click_button "Save changes"
end
- it "shows page with new data" do
+ it "shows page with new data" do
expect(page).to have_content('bigbang@mail.com')
expect(page).to have_content('Big Bang')
end
it "changes user entry" do
- @simple_user.reload
- expect(@simple_user.name).to eq('Big Bang')
- expect(@simple_user.is_admin?).to be_truthy
- expect(@simple_user.password_expires_at).to be <= Time.now
+ user.reload
+ expect(user.name).to eq('Big Bang')
+ expect(user.is_admin?).to be_truthy
+ expect(user.password_expires_at).to be <= Time.now
+ end
+ end
+
+ describe 'update username to non ascii char' do
+ it do
+ fill_in 'user_username', with: '\u3042\u3044'
+ click_button('Save')
+
+ page.within '#error_explanation' do
+ expect(page).to have_content('Username')
+ end
+
+ expect(page).to have_selector(%(form[action="/admin/users/#{user.username}"]))
end
end
end
describe "GET /admin/users/:id/projects" do
+ let(:group) { create(:group) }
+ let!(:project) { create(:project, group: group) }
+
before do
- @group = create(:group)
- @project = create(:project, group: @group)
- @simple_user = create(:user)
- @group.add_developer(@simple_user)
+ group.add_developer(user)
- visit projects_admin_user_path(@simple_user)
+ visit projects_admin_user_path(user)
end
it "lists group projects" do
within(:css, '.append-bottom-default + .panel') do
expect(page).to have_content 'Group projects'
- expect(page).to have_link @group.name, admin_group_path(@group)
+ expect(page).to have_link group.name, admin_group_path(group)
end
end
it 'allows navigation to the group details' do
within(:css, '.append-bottom-default + .panel') do
- click_link @group.name
+ click_link group.name
end
within(:css, 'h3.page-title') do
- expect(page).to have_content "Group: #{@group.name}"
+ expect(page).to have_content "Group: #{group.name}"
end
- expect(page).to have_content @project.name
+ expect(page).to have_content project.name
end
it 'shows the group access level' do
@@ -270,4 +284,99 @@ describe "Admin::Users", feature: true do
expect(page).not_to have_selector('.group_member')
end
end
+
+ describe 'show user attributes' do
+ it do
+ visit admin_users_path
+
+ click_link user.name
+
+ expect(page).to have_content 'Account'
+ expect(page).to have_content 'Personal projects limit'
+ end
+ end
+
+ describe 'remove users secondary email', js: true do
+ let!(:secondary_email) do
+ create :email, email: 'secondary@example.com', user: user
+ end
+
+ it do
+ visit admin_user_path(user.username)
+
+ expect(page).to have_content("Secondary email: #{secondary_email.email}")
+
+ find("#remove_email_#{secondary_email.id}").click
+
+ expect(page).not_to have_content(secondary_email.email)
+ end
+ end
+
+ describe 'show user keys' do
+ let!(:key1) do
+ create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1")
+ end
+
+ let!(:key2) do
+ create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2")
+ end
+
+ it do
+ visit admin_users_path
+
+ click_link user.name
+ click_link 'SSH keys'
+
+ expect(page).to have_content(key1.title)
+ expect(page).to have_content(key2.title)
+
+ click_link key2.title
+
+ expect(page).to have_content(key2.title)
+ expect(page).to have_content(key2.key)
+
+ click_link 'Remove'
+
+ expect(page).not_to have_content(key2.title)
+ end
+ end
+
+ describe 'show user identities' do
+ it 'shows user identities' do
+ visit admin_user_identities_path(user)
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('twitter')
+ end
+ end
+
+ describe 'update user identities' do
+ before do
+ allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
+ end
+
+ it 'modifies twitter identity' do
+ visit admin_user_identities_path(user)
+
+ find('.table').find(:link, 'Edit').click
+ fill_in 'identity_extern_uid', with: '654321'
+ select 'twitter_updated', from: 'identity_provider'
+ click_button 'Save changes'
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content('twitter_updated')
+ expect(page).to have_content('654321')
+ end
+ end
+
+ describe 'remove user with identities' do
+ it 'removes user with twitter identity' do
+ visit admin_user_identities_path(user)
+
+ click_link 'Delete'
+
+ expect(page).to have_content(user.name)
+ expect(page).not_to have_content('twitter')
+ end
+ end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index c16aafa1470..188d33e8ef4 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -125,7 +125,9 @@ describe 'Issue Boards', feature: true, js: true do
first('.card').click
end
- page.within('.assignee') do
+ page.within(find('.assignee')) do
+ expect(page).to have_content('No assignee')
+
click_link 'assign yourself'
wait_for_vue_resource
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index e48a2b0c92e..0648c89a5c7 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -3,7 +3,6 @@ require 'spec_helper'
feature 'Cycle Analytics', feature: true, js: true do
include WaitForAjax
- let(:project) { create(:project) }
let(:user) { create(:user) }
let(:guest) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
new file mode 100644
index 00000000000..7d59fcac517
--- /dev/null
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Active Tab', feature: true do
+ before do
+ login_as :user
+ end
+
+ shared_examples 'page has active tab' do |title|
+ it "#{title} tab" do
+ expect(page).to have_selector('.nav-sidebar li.active', count: 1)
+ expect(find('.nav-sidebar li.active')).to have_content(title)
+ end
+ end
+
+ context 'on dashboard projects' do
+ before do
+ visit dashboard_projects_path
+ end
+
+ it_behaves_like 'page has active tab', 'Projects'
+ end
+
+ context 'on dashboard issues' do
+ before do
+ visit issues_dashboard_path
+ end
+
+ it_behaves_like 'page has active tab', 'Issues'
+ end
+
+ context 'on dashboard merge requests' do
+ before do
+ visit merge_requests_dashboard_path
+ end
+
+ it_behaves_like 'page has active tab', 'Merge Requests'
+ end
+
+ context 'on dashboard groups' do
+ before do
+ visit dashboard_groups_path
+ end
+
+ it_behaves_like 'page has active tab', 'Groups'
+ end
+end
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
new file mode 100644
index 00000000000..038c1641be9
--- /dev/null
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Archived Project', feature: true do
+ let(:user) { create :user }
+ let(:project) { create :project}
+ let(:archived_project) { create(:project, :archived) }
+
+ before do
+ project.team << [user, :master]
+ archived_project.team << [user, :master]
+
+ login_as(user)
+
+ visit dashboard_projects_path
+ end
+
+ it 'renders non archived projects' do
+ expect(page).to have_link(project.name)
+ expect(page).not_to have_link(archived_project.name)
+ end
+
+ it 'renders all projects' do
+ click_link 'Show archived projects'
+
+ expect(page).to have_link(project.name)
+ expect(page).to have_link(archived_project.name)
+ end
+end
diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb
new file mode 100644
index 00000000000..d5f8470fab0
--- /dev/null
+++ b/spec/features/dashboard/group_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Group', feature: true do
+ before do
+ login_as(:user)
+ end
+
+ it 'creates new grpup' do
+ visit dashboard_groups_path
+ click_link 'New Group'
+
+ fill_in 'group_path', with: 'Samurai'
+ fill_in 'group_description', with: 'Tokugawa Shogunate'
+ click_button 'Create group'
+
+ expect(current_path).to eq group_path(Group.find_by(name: 'Samurai'))
+ expect(page).to have_content('Samurai')
+ expect(page).to have_content('Tokugawa Shogunate')
+ end
+end
diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb
new file mode 100644
index 00000000000..2803f7ec62b
--- /dev/null
+++ b/spec/features/dashboard/help_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+RSpec.describe 'Dashboard Help', feature: true do
+ before do
+ login_as(:user)
+ end
+
+ it 'renders correctly markdown' do
+ visit help_page_path("administration/raketasks/maintenance")
+
+ expect(page).to have_content('Gather information about GitLab and the system it runs on')
+
+ node = find('.documentation h2 a#user-content-check-gitlab-configuration')
+ expect(node[:href]).to eq '#check-gitlab-configuration'
+ expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration'
+ end
+end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 3934c936f20..8b3e2fa93a2 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -4,10 +4,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
include WaitForAjax
let(:branch) { 'expand-collapse-diffs' }
+ let(:project) { create(:project) }
before do
login_as :admin
- project = create(:project)
# Ensure that undiffable.md is in .gitattributes
project.repository.copy_gitattributes(branch)
@@ -31,6 +31,33 @@ feature 'Expand and collapse diffs', js: true, feature: true do
define_method(file.split('.').first) { file_container(file) }
end
+ it 'should show the diff content with a highlighted line when linking to line' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+
+ visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: "#{large_diff[:id]}_0_1")
+ execute_script('window.location.reload()')
+
+ wait_for_ajax
+
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ expect(large_diff).to have_selector('.hll')
+ end
+
+ it 'should show the diff content when linking to file' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+
+ visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: large_diff[:id])
+ execute_script('window.location.reload()')
+
+ wait_for_ajax
+
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
context 'visiting a commit with collapsed diffs' do
it 'shows small diffs immediately' do
expect(small_diff).to have_selector('.code')
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/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
deleted file mode 100644
index 9dfa5d1de19..00000000000
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-require 'rails_helper'
-
-feature 'Issue filtering by Milestone', feature: true do
- let(:project) { create(:project, :public) }
- let(:milestone) { create(:milestone, project: project) }
-
- scenario 'filters by no Milestone', js: true do
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::None.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone')
- expect(page).to have_css('.issue', count: 1)
- end
-
- context 'filters by upcoming milestone', js: true do
- it 'does not show issues with no expiry' do
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::Upcoming.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
- expect(page).to have_css('.issue', count: 0)
- end
-
- it 'shows issues in future' do
- milestone = create(:milestone, project: project, due_date: Date.tomorrow)
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::Upcoming.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
- expect(page).to have_css('.issue', count: 1)
- end
-
- it 'does not show issues in past' do
- milestone = create(:milestone, project: project, due_date: Date.yesterday)
- create(:issue, project: project)
- create(:issue, project: project, milestone: milestone)
-
- visit_issues(project)
- filter_by_milestone(Milestone::Upcoming.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
- expect(page).to have_css('.issue', count: 0)
- end
- end
-
- scenario 'filters by a specific Milestone', js: true do
- create(:issue, project: project, milestone: milestone)
- create(:issue, project: project)
-
- visit_issues(project)
- filter_by_milestone(milestone.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
- expect(page).to have_css('.issue', count: 1)
- end
-
- context 'when milestone has single quotes in title' do
- background do
- milestone.update(name: "rock 'n' roll")
- end
-
- scenario 'filters by a specific Milestone', js: true do
- create(:issue, project: project, milestone: milestone)
- create(:issue, project: project)
-
- visit_issues(project)
- filter_by_milestone(milestone.title)
-
- expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
- expect(page).to have_css('.issue', count: 1)
- end
- end
-
- def visit_issues(project)
- visit namespace_project_issues_path(project.namespace, project)
- end
-
- def filter_by_milestone(title)
- find(".js-milestone-select").click
- find(".milestone-filter .dropdown-content a", text: title).click
- end
-end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
new file mode 100644
index 00000000000..16dcc487812
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -0,0 +1,188 @@
+require 'rails_helper'
+
+describe 'Dropdown assignee', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
+ let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ sleep 5
+ wait_for_ajax
+ end
+ end
+
+ def dropdown_assignee_size
+ page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_assignee(text)
+ find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [user_john, :master]
+ project.team << [user_jacob, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has assignee:' do
+ filtered_search.set('assignee:')
+
+ 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()
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('assignee:')
+
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('assignee:')
+
+ expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading')
+ end
+
+ it 'should load all the assignees when opened' do
+ send_keys_to_filtered_search('assignee:')
+
+ 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
+ before do
+ send_keys_to_filtered_search('assignee:')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search('j')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search('J')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by username with symbol' do
+ send_keys_to_filtered_search('@ot')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by case insensitive username with symbol' do
+ send_keys_to_filtered_search('@OT')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by username without symbol' do
+ send_keys_to_filtered_search('ot')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+
+ it 'filters by case insensitive username without symbol' do
+ send_keys_to_filtered_search('OT')
+
+ expect(dropdown_assignee_size).to eq(2)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before 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}")
+ end
+
+ it 'fills in the assignee username when the assignee has been filtered' do
+ send_keys_to_filtered_search('roo')
+ click_assignee(user.name)
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ 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")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens assignee dropdown with existing search term' do
+ filtered_search.set('searchTerm assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+
+ it 'opens assignee dropdown with existing author' do
+ filtered_search.set('author:@user assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+
+ it 'opens assignee dropdown with existing label' do
+ filtered_search.set('label:~bug assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+
+ it 'opens assignee dropdown with existing milestone' do
+ filtered_search.set('milestone:%v1.0 assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
new file mode 100644
index 00000000000..464749d01e3
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -0,0 +1,160 @@
+require 'rails_helper'
+
+describe 'Dropdown author', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
+ let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_author) { '#js-dropdown-author' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ sleep 5
+ wait_for_ajax
+ end
+ end
+
+ def dropdown_author_size
+ page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_author(text)
+ find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [user_john, :master]
+ project.team << [user_jacob, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has author:' do
+ filtered_search.set('author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click()
+
+ expect(page).to have_css(js_dropdown_author, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('author:')
+
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('author:')
+
+ expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading')
+ end
+
+ it 'should load all the authors when opened' do
+ send_keys_to_filtered_search('author:')
+
+ 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
+ before do
+ filtered_search.set('author')
+ send_keys_to_filtered_search(':')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search('ja')
+
+ expect(dropdown_author_size).to eq(1)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search('Ja')
+
+ expect(dropdown_author_size).to eq(1)
+ end
+
+ it 'filters by username with symbol' do
+ send_keys_to_filtered_search('@ot')
+
+ expect(dropdown_author_size).to eq(2)
+ end
+
+ it 'filters by username without symbol' do
+ send_keys_to_filtered_search('ot')
+
+ expect(dropdown_author_size).to eq(2)
+ end
+
+ it 'filters by case insensitive username without symbol' do
+ send_keys_to_filtered_search('OT')
+
+ expect(dropdown_author_size).to eq(2)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before do
+ filtered_search.set('author')
+ send_keys_to_filtered_search(':')
+ end
+
+ it 'fills in the author username when the author has not been filtered' 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}")
+ 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}")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens author dropdown with existing search term' do
+ filtered_search.set('searchTerm author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'opens author dropdown with existing assignee' do
+ filtered_search.set('assignee:@user author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'opens author dropdown with existing label' do
+ filtered_search.set('label:~bug author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+
+ it 'opens author dropdown with existing milestone' do
+ filtered_search.set('milestone:%v1.0 author:')
+
+ expect(page).to have_css(js_dropdown_author, visible: true)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
new file mode 100644
index 00000000000..04dd54ab459
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -0,0 +1,134 @@
+require 'rails_helper'
+
+describe 'Dropdown hint', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_hint) { '#js-dropdown-hint' }
+
+ def dropdown_hint_size
+ page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_hint(text)
+ find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ before do
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ filtered_search.click
+ end
+
+ it 'opens when the search bar is first focused' do
+ expect(page).to have_css(js_dropdown_hint, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ end
+ end
+
+ describe 'filtering' do
+ it 'does not filter `Keep typing and press Enter`' do
+ filtered_search.set('randomtext')
+
+ expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false)
+ expect(dropdown_hint_size).to eq(0)
+ end
+
+ it 'filters with text' do
+ filtered_search.set('a')
+
+ expect(dropdown_hint_size).to eq(3)
+ end
+ end
+
+ describe 'selecting from dropdown with no input' do
+ before do
+ filtered_search.click
+ end
+
+ it 'opens the author dropdown when you click on author' do
+ click_hint('author')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ expect(filtered_search.value).to eq('author:')
+ end
+
+ it 'opens the assignee dropdown when you click on assignee' do
+ click_hint('assignee')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ expect(filtered_search.value).to eq('assignee:')
+ end
+
+ it 'opens the milestone dropdown when you click on milestone' do
+ click_hint('milestone')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ expect(filtered_search.value).to eq('milestone:')
+ end
+
+ it 'opens the label dropdown when you click on label' do
+ click_hint('label')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ expect(filtered_search.value).to eq('label:')
+ end
+ end
+
+ describe 'selecting from dropdown with some input' do
+ it 'opens the author dropdown when you click on author' do
+ filtered_search.set('auth')
+ click_hint('author')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ expect(filtered_search.value).to eq('author:')
+ end
+
+ it 'opens the assignee dropdown when you click on assignee' do
+ filtered_search.set('assign')
+ click_hint('assignee')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ expect(filtered_search.value).to eq('assignee:')
+ end
+
+ it 'opens the milestone dropdown when you click on milestone' do
+ filtered_search.set('mile')
+ click_hint('milestone')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ expect(filtered_search.value).to eq('milestone:')
+ end
+
+ it 'opens the label dropdown when you click on label' do
+ filtered_search.set('lab')
+ click_hint('label')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ expect(filtered_search.value).to eq('label:')
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
new file mode 100644
index 00000000000..89c144141c9
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -0,0 +1,242 @@
+require 'rails_helper'
+
+describe 'Dropdown label', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+ let!(:bug_label) { create(:label, project: project, title: 'bug') }
+ let!(:uppercase_label) { create(:label, project: project, title: 'BUG') }
+ let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
+ let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
+ let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
+ let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')}
+ let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')}
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_label) { '#js-dropdown-label' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ sleep 3
+ wait_for_ajax
+ sleep 3
+ end
+ end
+
+ def dropdown_label_size
+ page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_label(text)
+ find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has label:' do
+ filtered_search.set('label:')
+
+ expect(page).to have_css(js_dropdown_label, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click()
+
+ expect(page).to have_css(js_dropdown_label, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('label:')
+
+ expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('label:')
+
+ expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading')
+ end
+
+ it 'should load all the labels when opened' do
+ send_keys_to_filtered_search('label:')
+
+ expect(dropdown_label_size).to be > 0
+ end
+ end
+
+ describe 'filtering' do
+ before do
+ filtered_search.set('label')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search(':b')
+
+ expect(dropdown_label_size).to eq(2)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search(':B')
+
+ expect(dropdown_label_size).to eq(2)
+ end
+
+ it 'filters by name with symbol' do
+ send_keys_to_filtered_search(':~bu')
+
+ expect(dropdown_label_size).to eq(2)
+ end
+
+ it 'filters by case insensitive name with symbol' do
+ send_keys_to_filtered_search(':~BU')
+
+ expect(dropdown_label_size).to eq(2)
+ end
+
+ it 'filters by multiple words' do
+ send_keys_to_filtered_search(':Hig')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by multiple words with symbol' do
+ send_keys_to_filtered_search(':~Hig')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by multiple words containing single quotes' do
+ send_keys_to_filtered_search(':won\'t')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by multiple words containing single quotes with symbol' do
+ send_keys_to_filtered_search(':~won\'t')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by multiple words containing double quotes' do
+ send_keys_to_filtered_search(':won"t')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by multiple words containing double quotes with symbol' do
+ send_keys_to_filtered_search(':~won"t')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by special characters' do
+ send_keys_to_filtered_search(':^+')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+
+ it 'filters by special characters with symbol' do
+ send_keys_to_filtered_search(':~^+')
+
+ expect(dropdown_label_size).to eq(1)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before do
+ filtered_search.set('label:')
+ end
+
+ it 'fills in the label name when the label has not been filled' 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}")
+ end
+
+ it 'fills in the label name when the label is partially filled' do
+ send_keys_to_filtered_search('bu')
+ 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}")
+ 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}\"")
+ 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}\"")
+ 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}'")
+ 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}")
+ 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}")
+ 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")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens label dropdown with existing search term' do
+ filtered_search.set('searchTerm label:')
+ expect(page).to have_css(js_dropdown_label, visible: true)
+ end
+
+ it 'opens label dropdown with existing author' do
+ filtered_search.set('author:@person label:')
+ expect(page).to have_css(js_dropdown_label, visible: true)
+ end
+
+ it 'opens label dropdown with existing assignee' do
+ filtered_search.set('assignee:@person label:')
+ expect(page).to have_css(js_dropdown_label, visible: true)
+ end
+
+ it 'opens label dropdown with existing label' do
+ filtered_search.set('label:~urgent label:')
+ expect(page).to have_css(js_dropdown_label, visible: true)
+ end
+
+ it 'opens label dropdown with existing milestone' do
+ filtered_search.set('milestone:%v2.0 label:')
+ expect(page).to have_css(js_dropdown_label, visible: true)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
new file mode 100644
index 00000000000..e5a271b663f
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -0,0 +1,222 @@
+require 'rails_helper'
+
+describe 'Dropdown milestone', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+ let!(:milestone) { create(:milestone, title: 'v1.0', project: project) }
+ let!(:uppercase_milestone) { create(:milestone, title: 'CAP_MILESTONE', project: project) }
+ let!(:two_words_milestone) { create(:milestone, title: 'Future Plan', project: project) }
+ let!(:wont_fix_milestone) { create(:milestone, title: 'Won"t Fix', project: project) }
+ let!(:special_milestone) { create(:milestone, title: '!@#$%^&*(+)', project: project) }
+ let!(:long_milestone) { create(:milestone, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title', project: project) }
+
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_milestone) { '#js-dropdown-milestone' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ sleep 3
+ wait_for_ajax
+ sleep 3
+ end
+ end
+
+ def dropdown_milestone_size
+ page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_milestone(text)
+ find('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ def click_static_milestone(text)
+ find('#js-dropdown-milestone .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has milestone:' do
+ filtered_search.set('milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click()
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('milestone:')
+
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('milestone:')
+
+ expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading')
+ end
+
+ it 'should load all the milestones when opened' do
+ send_keys_to_filtered_search('milestone:')
+
+ expect(dropdown_milestone_size).to be > 0
+ end
+ end
+
+ describe 'filtering' do
+ before do
+ filtered_search.set('milestone')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search(':v1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search(':V1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by name with symbol' do
+ send_keys_to_filtered_search(':%v1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by case insensitive name with symbol' do
+ send_keys_to_filtered_search(':%V1')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by special characters' do
+ send_keys_to_filtered_search(':(+')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+
+ it 'filters by special characters with symbol' do
+ send_keys_to_filtered_search(':%(+')
+
+ expect(dropdown_milestone_size).to eq(1)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before do
+ filtered_search.set('milestone:')
+ end
+
+ it 'fills in the milestone name when the milestone has not been filled' do
+ click_milestone(milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect(filtered_search.value).to eq("milestone:%#{milestone.title}")
+ end
+
+ it 'fills in the milestone name when the milestone is partially filled' do
+ send_keys_to_filtered_search('v')
+ click_milestone(milestone.title)
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ 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}\"")
+ 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}\"")
+ 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}'")
+ 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}")
+ 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}")
+ 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")
+ 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")
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens milestone dropdown with existing search term' do
+ filtered_search.set('searchTerm milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing author' do
+ filtered_search.set('author:@john milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing assignee' do
+ filtered_search.set('assignee:@john milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing label' do
+ filtered_search.set('label:~important milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+
+ it 'opens milestone dropdown with existing milestone' do
+ filtered_search.set('milestone:%100 milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
new file mode 100644
index 00000000000..1cdac520181
--- /dev/null
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -0,0 +1,804 @@
+require 'rails_helper'
+
+describe 'Filter issues', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project, group: group) }
+ let!(:user) { create(:user) }
+ let!(:user2) { create(:user) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+ let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
+
+ let!(:bug_label) { create(:label, project: project, title: 'bug') }
+ let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
+ let!(:milestone) { create(:milestone, title: "8", project: project) }
+ let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
+
+ 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, submit: true)
+ filtered_search.set(search_term)
+
+ if submit
+ filtered_search.send_keys(:enter)
+ end
+ end
+
+ def expect_filtered_search_input(input)
+ expect(find('.filtered-search').value).to eq(input)
+ end
+
+ def expect_no_issues_list
+ page.within '.issues-list' do
+ expect(page).not_to have_selector('.issue')
+ end
+ end
+
+ def expect_issues_list_count(open_count, closed_count = 0)
+ all_count = open_count + closed_count
+
+ expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: open_count)
+ 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]
+ group.add_developer(user)
+ group.add_developer(user2)
+ login_as(user)
+ create(:issue, project: project)
+
+ create(:issue, title: "Bug report 1", project: project)
+ create(:issue, title: "Bug report 2", project: project)
+ create(:issue, title: "issue with 'single quotes'", project: project)
+ create(:issue, title: "issue with \"double quotes\"", project: project)
+ create(:issue, title: "issue with !@\#{$%^&*()-+", project: project)
+ create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user)
+ create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user)
+
+ issue = create(:issue,
+ title: "Bug 2",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue.labels << bug_label
+
+ issue_with_caps_label = create(:issue,
+ title: "issue by assignee with searchTerm and label",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue_with_caps_label.labels << caps_sensitive_label
+
+ issue_with_everything = create(:issue,
+ title: "Bug report with everything you thought was possible",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue_with_everything.labels << bug_label
+ issue_with_everything.labels << caps_sensitive_label
+
+ multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project)
+ multiple_words_label_issue.labels << multiple_words_label
+
+ future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month)
+
+ create(:issue,
+ title: "Issue with future milestone",
+ milestone: future_milestone,
+ project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'filter issues by author' do
+ context 'only author' do
+ it 'filters issues by searched author' do
+ input_filtered_search("author:@#{user.username}")
+
+ expect_issues_list_count(5)
+ end
+
+ it 'filters issues by invalid author' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple authors' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+
+ context 'author with other filters' do
+ it 'filters issues by searched author and text' do
+ search = "author:@#{user.username} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched author, assignee and text' do
+ search = "author:@#{user.username} assignee:@#{user.username} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched author, assignee, label, and text' do
+ search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched author, assignee, label, milestone and text' do
+ search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ it 'sorting' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+
+ describe 'filter issues by assignee' do
+ context 'only assignee' do
+ it 'filters issues by searched assignee' do
+ search = "assignee:@#{user.username}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(5)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by no assignee' do
+ search = "assignee:none"
+ input_filtered_search(search)
+
+ expect_issues_list_count(8, 1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by invalid assignee' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple assignees' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+
+ context 'assignee with other filters' do
+ it 'filters issues by searched assignee and text' do
+ search = "assignee:@#{user.username} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched assignee, author and text' do
+ search = "assignee:@#{user.username} author:@#{user.username} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched assignee, author, label, text' do
+ search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched assignee, author, label, milestone and text' do
+ search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+ end
+
+ describe 'filter issues by label' do
+ context 'only label' do
+ it 'filters issues by searched label' do
+ search = "label:~#{bug_label.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by no label' do
+ search = "label:none"
+ input_filtered_search(search)
+
+ expect_issues_list_count(9, 1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by invalid label' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple labels' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by label containing special characters' do
+ special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}')
+ special_issue = create(:issue, title: "Issue with special character label", project: project)
+ special_issue.labels << special_label
+
+ search = "label:~#{special_label.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'does not show issues' do
+ new_label = create(:label, project: project, title: "new_label")
+
+ search = "label:~#{new_label.title}"
+ input_filtered_search(search)
+
+ expect_no_issues_list()
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'label with multiple words' do
+ it 'special characters' do
+ special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce")
+ special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project)
+ special_multiple_issue.labels << special_multiple_label
+
+ search = "label:~'#{special_multiple_label.title}'"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+
+ # filtered search defaults quotations to double quotes
+ expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"")
+ end
+
+ it 'single quotes' do
+ search = "label:~'#{multiple_words_label.title}'"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"")
+ end
+
+ it 'double quotes' do
+ search = "label:~\"#{multiple_words_label.title}\""
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'single quotes containing double quotes' do
+ double_quotes_label = create(:label, project: project, title: 'won"t fix')
+ double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project)
+ double_quotes_label_issue.labels << double_quotes_label
+
+ search = "label:~'#{double_quotes_label.title}'"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'double quotes containing single quotes' do
+ single_quotes_label = create(:label, project: project, title: "won't fix")
+ single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project)
+ single_quotes_label_issue.labels << single_quotes_label
+
+ search = "label:~\"#{single_quotes_label.title}\""
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'label with other filters' do
+ it 'filters issues by searched label and text' do
+ search = "label:~#{caps_sensitive_label.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, author and text' do
+ search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, author, assignee and text' do
+ search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, author, assignee, milestone and text' do
+ search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'multiple labels with other filters' do
+ it 'filters issues by searched label, label2, and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, label2, author and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, label2, author, assignee and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched label, label2, author, assignee, milestone and text' do
+ search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'issue label clicked' do
+ before do
+ find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click
+ sleep 1
+ end
+
+ it 'filters' do
+ expect_issues_list_count(1)
+ end
+
+ it 'displays in search bar' do
+ expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"")
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ end
+ end
+
+ describe 'filter issues by milestone' do
+ context 'only milestone' do
+ it 'filters issues by searched milestone' do
+ input_filtered_search("milestone:%#{milestone.title}")
+
+ expect_issues_list_count(5)
+ end
+
+ it 'filters issues by no milestone' do
+ input_filtered_search("milestone:none")
+
+ expect_issues_list_count(7, 1)
+ end
+
+ it 'filters issues by upcoming milestones' do
+ input_filtered_search("milestone:upcoming")
+
+ expect_issues_list_count(1)
+ end
+
+ it 'filters issues by invalid milestones' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by multiple milestones' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+
+ it 'filters issues by milestone containing special characters' do
+ special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
+ create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
+
+ search = "milestone:%#{special_milestone.title}"
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'does not show issues' do
+ new_milestone = create(:milestone, title: "new", project: project)
+
+ search = "milestone:%#{new_milestone.title}"
+ input_filtered_search(search)
+
+ expect_no_issues_list()
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'milestone with other filters' do
+ it 'filters issues by searched milestone and text' do
+ search = "milestone:%#{milestone.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched milestone, author and text' do
+ search = "milestone:%#{milestone.title} author:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched milestone, author, assignee and text' do
+ search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched milestone, author, assignee, label and text' do
+ search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug"
+ input_filtered_search(search)
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts' do
+ pending('to be tested, issue #26546')
+ expect(true).to be(false)
+ end
+ 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
+ search = 'Bug'
+ input_filtered_search(search)
+
+ expect_issues_list_count(4, 1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by multiple searched text' do
+ search = 'Bug report'
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by case insensitive searched text' do
+ search = 'bug report'
+ input_filtered_search(search)
+
+ expect_issues_list_count(3)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched text containing single quotes' do
+ search = '\'single quotes\''
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched text containing double quotes' do
+ search = '"double quotes"'
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'filters issues by searched text containing special characters' do
+ search = '!@#{$%^&*()-+'
+ input_filtered_search(search)
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search)
+ end
+
+ it 'does not show any issues' do
+ search = 'testing'
+ input_filtered_search(search)
+
+ expect_no_issues_list()
+ expect_filtered_search_input(search)
+ end
+ end
+
+ context 'searched text with other filters' do
+ it 'filters issues by searched text and author' do
+ input_filtered_search("bug author:@#{user.username}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} bug")
+ end
+
+ it 'filters issues by searched text, author and more text' do
+ input_filtered_search("bug author:@#{user.username} report")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} bug report")
+ end
+
+ it 'filters issues by searched text, author and assignee' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug")
+ end
+
+ it 'filters issues by searched text, author, more text and assignee' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report")
+ end
+
+ it 'filters issues by searched text, author, more text, assignee and even more text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with")
+ end
+
+ it 'filters issues by searched text, author, assignee and label' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug")
+ end
+
+ it 'filters issues by searched text, author, text, assignee, text, label and text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything")
+ end
+
+ it 'filters issues by searched text, author, assignee, label and milestone' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}")
+
+ expect_issues_list_count(2)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug")
+ end
+
+ it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you")
+ end
+
+ it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug")
+ end
+
+ it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
+
+ expect_issues_list_count(1)
+ expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought")
+ end
+ end
+
+ context 'sorting' do
+ it 'sorts by oldest updated' do
+ create(:issue,
+ title: '3 days ago',
+ project: project,
+ author: user,
+ created_at: 3.days.ago,
+ updated_at: 3.days.ago)
+
+ old_issue = create(:issue,
+ title: '5 days ago',
+ project: project,
+ author: user,
+ created_at: 5.days.ago,
+ updated_at: 5.days.ago)
+
+ input_filtered_search('days ago')
+
+ expect_issues_list_count(2)
+
+ sort_toggle = find('.filtered-search-container .dropdown-toggle')
+ sort_toggle.click
+
+ find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click
+ wait_for_ajax
+
+ expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title)
+ end
+ end
+ end
+
+ describe 'retains filter when switching issue states' do
+ before do
+ input_filtered_search('bug')
+
+ # Wait for search results to load
+ sleep 2
+ end
+
+ it 'open state' do
+ find('.issues-state-filters a', text: 'Closed').click
+ wait_for_ajax
+
+ find('.issues-state-filters a', text: 'Open').click
+ wait_for_ajax
+
+ expect(page).to have_selector('.issues-list .issue', count: 4)
+ end
+
+ it 'closed state' do
+ find('.issues-state-filters a', text: 'Closed').click
+ wait_for_ajax
+
+ expect(page).to have_selector('.issues-list .issue', count: 1)
+ expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title)
+ end
+
+ it 'all state' do
+ find('.issues-state-filters a', text: 'All').click
+ wait_for_ajax
+
+ expect(page).to have_selector('.issues-list .issue', count: 5)
+ end
+ end
+
+ describe 'RSS feeds' do
+ it 'updates atom feed link for project issues' do
+ visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id)
+ link = find('.nav-controls a', text: 'Subscribe')
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('milestone_title' => [milestone.title])
+ expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(auto_discovery_params).to include('private_token' => [user.private_token])
+ expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
+ expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ end
+
+ it 'updates atom feed link for group issues' do
+ visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id)
+ link = find('.nav-controls a', text: 'Subscribe')
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expect(params).to include('private_token' => [user.private_token])
+ expect(params).to include('milestone_title' => [milestone.title])
+ expect(params).to include('assignee_id' => [user.id.to_s])
+ expect(auto_discovery_params).to include('private_token' => [user.private_token])
+ expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
+ expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
new file mode 100644
index 00000000000..56b1d354eb0
--- /dev/null
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -0,0 +1,88 @@
+require 'rails_helper'
+
+describe 'Search bar', js: true, feature: true do
+ include WaitForAjax
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user) }
+ let(:filtered_search) { find('.filtered-search') }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ def get_left_style(style)
+ left_style = /left:\s\d*[.]\d*px/.match(style)
+ left_style.to_s.gsub('left: ', '').to_f
+ end
+
+ describe 'clear search button' do
+ it 'clears text' do
+ search_text = 'search_text'
+ filtered_search.set(search_text)
+
+ expect(filtered_search.value).to eq(search_text)
+ find('.filtered-search-input-container .clear-search').click
+
+ expect(filtered_search.value).to eq('')
+ end
+
+ it 'hides by default' do
+ expect(page).to have_css('.clear-search', visible: false)
+ end
+
+ it 'hides after clicked' do
+ filtered_search.set('a')
+ find('.filtered-search-input-container .clear-search').click
+
+ expect(page).to have_css('.clear-search', visible: false)
+ end
+
+ it 'hides when there is no text' do
+ filtered_search.set('a')
+ filtered_search.set('')
+
+ expect(page).to have_css('.clear-search', visible: false)
+ end
+
+ it 'shows when there is text' do
+ filtered_search.set('a')
+
+ expect(page).to have_css('.clear-search', visible: true)
+ end
+
+ it 'resets the dropdown hint filter' do
+ filtered_search.click
+ original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
+
+ filtered_search.set('author')
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1)
+
+ find('.filtered-search-input-container .clear-search').click
+ filtered_search.click
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size)
+ end
+
+ it 'resets the dropdown filters' do
+ filtered_search.set('a')
+ hint_style = page.find('#js-dropdown-hint')['style']
+ hint_offset = get_left_style(hint_style)
+
+ filtered_search.set('author:')
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0)
+
+ find('.filtered-search-input-container .clear-search').click
+ filtered_search.click
+
+ expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0
+ expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset)
+ end
+ end
+end
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/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
deleted file mode 100644
index c9a3ecf16ea..00000000000
--- a/spec/features/issues/reset_filters_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'rails_helper'
-
-feature 'Issues filter reset button', feature: true, js: true do
- include WaitForAjax
- include IssueHelpers
-
- let!(:project) { create(:project, :public) }
- let!(:user) { create(:user)}
- let!(:milestone) { create(:milestone, project: project) }
- let!(:bug) { create(:label, project: project, name: 'bug')}
- let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')}
- let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')}
-
- before do
- project.team << [user, :developer]
- end
-
- context 'when a milestone filter has been applied' do
- it 'resets the milestone filter' do
- visit_issues(project, milestone_title: milestone.title)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when a label filter has been applied' do
- it 'resets the label filter' do
- visit_issues(project, label_name: bug.name)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when a text search has been conducted' do
- it 'resets the text search filter' do
- visit_issues(project, search: 'Bug')
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when author filter has been applied' do
- it 'resets the author filter' do
- visit_issues(project, author_id: user.id)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when assignee filter has been applied' do
- it 'resets the assignee filter' do
- visit_issues(project, assignee_id: user.id)
- expect(page).to have_css('.issue', count: 1)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when all filters have been applied' do
- it 'resets all filters' do
- visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
- expect(page).to have_css('.issue', count: 0)
-
- reset_filters
- expect(page).to have_css('.issue', count: 2)
- end
- end
-
- context 'when no filters have been applied' do
- it 'the reset link should not be visible' do
- visit_issues(project)
- expect(page).to have_css('.issue', count: 2)
- expect(page).not_to have_css '.reset_filters'
- end
- end
-
- def reset_filters
- find('.reset-filters').click
- end
-end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 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/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index c9a0059645d..4a6c76a5caf 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -22,4 +22,18 @@ feature 'Diffs URL', js: true, feature: true do
expect(page).to have_css('.diffs.tab-pane.active')
end
end
+
+ context 'when merge request has overflow' do
+ it 'displays warning' do
+ allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true)
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20)
+
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+
+ page.within('.alert') do
+ expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve
+ performance only 3 of 3+ files are displayed.")
+ end
+ end
+ end
end
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/issues/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 0253629f753..4c60329865c 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -7,25 +7,27 @@ feature 'Issue filtering by Labels', feature: true, js: true do
let!(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
- before do
- bug = create(:label, project: project, title: 'bug')
- feature = create(:label, project: project, title: 'feature')
- enhancement = create(:label, project: project, title: 'enhancement')
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature) { create(:label, project: project, title: 'feature') }
+ let!(:enhancement) { create(:label, project: project, title: 'enhancement') }
+
+ let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") }
+ let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") }
+ let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") }
- issue1 = create(:issue, title: "Bugfix1", project: project)
- issue1.labels << bug
+ before do
+ mr1.labels << bug
- issue2 = create(:issue, title: "Bugfix2", project: project)
- issue2.labels << bug
- issue2.labels << enhancement
+ mr2.labels << bug
+ mr2.labels << enhancement
- issue3 = create(:issue, title: "Feature1", project: project)
- issue3.labels << feature
+ mr3.title = "Feature1"
+ mr3.labels << feature
project.team << [user, :master]
login_as(user)
- visit namespace_project_issues_path(project.namespace, project)
+ visit namespace_project_merge_requests_path(project.namespace, project)
end
context 'filter by label bug' do
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 0d19563d628..4642b5a530d 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -1,10 +1,10 @@
require 'rails_helper'
-describe 'Filter issues', feature: true do
+describe 'Filter merge requests', feature: true do
include WaitForAjax
+ let!(:project) { create(:project) }
let!(:group) { create(:group) }
- let!(:project) { create(:project, group: group) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
@@ -14,12 +14,12 @@ describe 'Filter issues', feature: true do
project.team << [user, :master]
group.add_developer(user)
login_as(user)
- create(:issue, project: project)
+ create(:merge_request, source_project: project, target_project: project)
end
- describe 'for assignee from issues#index' do
+ describe 'for assignee from mr#index' do
before do
- visit namespace_project_issues_path(project.namespace, project)
+ visit namespace_project_merge_requests_path(project.namespace, project)
find('.js-assignee-search').click
@@ -47,9 +47,9 @@ describe 'Filter issues', feature: true do
end
end
- describe 'for milestone from issues#index' do
+ describe 'for milestone from mr#index' do
before do
- visit namespace_project_issues_path(project.namespace, project)
+ visit namespace_project_merge_requests_path(project.namespace, project)
find('.js-milestone-select').click
@@ -77,9 +77,9 @@ describe 'Filter issues', feature: true do
end
end
- describe 'for label from issues#index', js: true do
+ describe 'for label from mr#index', js: true do
before do
- visit namespace_project_issues_path(project.namespace, project)
+ visit namespace_project_merge_requests_path(project.namespace, project)
find('.js-label-select').click
wait_for_ajax
end
@@ -127,7 +127,7 @@ describe 'Filter issues', feature: true do
expect(page).to have_content wontfix.title
end
- find('.dropdown-menu-close-icon').click
+ find('body').click
expect(find('.filtered-labels')).to have_content(wontfix.title)
@@ -135,7 +135,7 @@ describe 'Filter issues', feature: true do
wait_for_ajax
find('.dropdown-menu-labels a', text: label.title).click
- find('.dropdown-menu-close-icon').click
+ find('body').click
expect(find('.filtered-labels')).to have_content(wontfix.title)
expect(find('.filtered-labels')).to have_content(label.title)
@@ -150,21 +150,21 @@ describe 'Filter issues', feature: true do
it "selects and unselects `won't fix`" do
find('.dropdown-menu-labels a', text: wontfix.title).click
find('.dropdown-menu-labels a', text: wontfix.title).click
-
- find('.dropdown-menu-close-icon').click
+ # Close label dropdown to load
+ find('body').click
expect(page).not_to have_css('.filtered-labels')
end
end
describe 'for assignee and label from issues#index' do
before do
- visit namespace_project_issues_path(project.namespace, project)
+ visit namespace_project_merge_requests_path(project.namespace, project)
find('.js-assignee-search').click
find('.dropdown-menu-user-link', text: user.username).click
- expect(page).not_to have_selector('.issues-list .issue')
+ expect(page).not_to have_selector('.mr-list .merge-request')
find('.js-label-select').click
@@ -196,38 +196,40 @@ describe 'Filter issues', feature: true do
end
end
- describe 'filter issues by text' do
+ describe 'filter merge requests by text' do
before do
- create(:issue, title: "Bug", project: project)
+ create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug")
bug_label = create(:label, project: project, title: 'bug')
milestone = create(:milestone, title: "8", project: project)
- issue = create(:issue,
- title: "Bug 2",
- project: project,
+ mr = create(:merge_request,
+ title: "Bug 2",
+ source_project: project,
+ target_project: project,
+ source_branch: "bug2",
milestone: milestone,
author: user,
assignee: user)
- issue.labels << bug_label
+ mr.labels << bug_label
- visit namespace_project_issues_path(project.namespace, project)
+ visit namespace_project_merge_requests_path(project.namespace, project)
end
context 'only text', js: true do
- it 'filters issues by searched text' do
+ it 'filters merge requests by searched text' do
fill_in 'issuable_search', with: 'Bug'
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
end
end
- it 'does not show any issues' do
+ it 'does not show any merge requests' do
fill_in 'issuable_search', with: 'testing'
- page.within '.issues-list' do
- expect(page).not_to have_selector('.issue')
+ page.within '.mr-list' do
+ expect(page).not_to have_selector('.merge-request')
end
end
end
@@ -237,8 +239,8 @@ describe 'Filter issues', feature: true do
fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
end
click_button 'Label'
@@ -248,8 +250,8 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-close-icon').click
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
end
end
@@ -257,8 +259,8 @@ describe 'Filter issues', feature: true do
fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
end
click_button 'Milestone'
@@ -267,8 +269,8 @@ describe 'Filter issues', feature: true do
end
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
end
end
@@ -276,8 +278,8 @@ describe 'Filter issues', feature: true do
fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
end
click_button 'Assignee'
@@ -286,8 +288,8 @@ describe 'Filter issues', feature: true do
end
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
end
end
@@ -295,8 +297,8 @@ describe 'Filter issues', feature: true do
fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
end
click_button 'Author'
@@ -305,26 +307,27 @@ describe 'Filter issues', feature: true do
end
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 1)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 1)
end
end
end
end
- describe 'filter issues and sort', js: true do
+ describe 'filter merge requests and sort', js: true do
before do
bug_label = create(:label, project: project, title: 'bug')
- bug_one = create(:issue, title: "Frontend", project: project)
- bug_two = create(:issue, title: "Bug 2", project: project)
- bug_one.labels << bug_label
- bug_two.labels << bug_label
+ mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend")
+ mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2")
- visit namespace_project_issues_path(project.namespace, project)
+ mr1.labels << bug_label
+ mr2.labels << bug_label
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
end
- it 'is able to filter and sort issues' do
+ it 'is able to filter and sort merge requests' do
click_button 'Label'
wait_for_ajax
page.within '.labels-filter' do
@@ -334,8 +337,8 @@ describe 'Filter issues', feature: true do
wait_for_ajax
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
- page.within '.issues-list' do
- expect(page).to have_selector('.issue', count: 2)
+ page.within '.mr-list' do
+ expect(page).to have_selector('.merge-request', count: 2)
end
click_button 'Last created'
@@ -344,41 +347,9 @@ describe 'Filter issues', feature: true do
end
wait_for_ajax
- page.within '.issues-list' do
+ page.within '.mr-list' do
expect(page).to have_content('Frontend')
end
end
end
-
- it 'updates atom feed link for project issues' do
- visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id)
-
- link = find('.nav-controls a', text: 'Subscribe')
- params = CGI::parse(URI.parse(link[:href]).query)
- auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
- auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
-
- expect(params).to include('private_token' => [user.private_token])
- expect(params).to include('milestone_title' => [''])
- expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('private_token' => [user.private_token])
- expect(auto_discovery_params).to include('milestone_title' => [''])
- expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
- end
-
- it 'updates atom feed link for group issues' do
- visit issues_group_path(group, milestone_title: '', assignee_id: user.id)
-
- link = find('.nav-controls a', text: 'Subscribe')
- params = CGI::parse(URI.parse(link[:href]).query)
- auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
- auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query)
-
- expect(params).to include('private_token' => [user.private_token])
- expect(params).to include('milestone_title' => [''])
- expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('private_token' => [user.private_token])
- expect(auto_discovery_params).to include('milestone_title' => [''])
- expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
- 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/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
new file mode 100644
index 00000000000..3a7ece7e1d6
--- /dev/null
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -0,0 +1,96 @@
+require 'rails_helper'
+
+feature 'Issues filter reset button', feature: true, js: true do
+ include WaitForAjax
+ include IssueHelpers
+
+ let!(:project) { create(:project, :public) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:bug) { create(:label, project: project, name: 'bug')}
+ let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) }
+ let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") }
+
+ let(:merge_request_css) { '.merge-request' }
+
+ before do
+ mr2.labels << bug
+ project.team << [user, :developer]
+ end
+
+ context 'when a milestone filter has been applied' do
+ it 'resets the milestone filter' do
+ visit_merge_requests(project, milestone_title: milestone.title)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when a label filter has been applied' do
+ it 'resets the label filter' do
+ visit_merge_requests(project, label_name: bug.name)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when a text search has been conducted' do
+ it 'resets the text search filter' do
+ visit_merge_requests(project, search: 'Bug')
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when author filter has been applied' do
+ it 'resets the author filter' do
+ visit_merge_requests(project, author_id: user.id)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when assignee filter has been applied' do
+ it 'resets the assignee filter' do
+ visit_merge_requests(project, assignee_id: user.id)
+ expect(page).to have_css(merge_request_css, count: 1)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when all filters have been applied' do
+ it 'resets all filters' do
+ visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+ expect(page).to have_css(merge_request_css, count: 0)
+
+ reset_filters
+ expect(page).to have_css(merge_request_css, count: 2)
+ end
+ end
+
+ context 'when no filters have been applied' do
+ it 'the reset link should not be visible' do
+ visit_merge_requests(project)
+ expect(page).to have_css(merge_request_css, count: 2)
+ expect(page).not_to have_css '.reset_filters'
+ end
+ end
+
+ def visit_merge_requests(project, opts = {})
+ visit namespace_project_merge_requests_path project.namespace, project, opts
+ end
+
+ def reset_filters
+ find('.reset-filters').click
+ end
+end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index b1b3a47a1ce..b13674b4db9 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -68,6 +68,51 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
end
end
+ describe 'merging the MR from the note' do
+ context 'when the current user can merge the MR' do
+ it 'merges the MR' do
+ write_note("/merge")
+
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload).to be_merged
+ end
+ end
+
+ context 'when the head diff changes in the meanwhile' do
+ before do
+ merge_request.source_branch = 'another_branch'
+ merge_request.save
+ end
+
+ it 'does not merge the MR' do
+ write_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+
+ context 'when the current user cannot merge the MR' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not merge the MR' do
+ write_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+ end
+
describe 'adding a due date from note' do
it 'does not recognize the command nor create a note' do
write_note('/due 2016-08-28')
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/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 8c4d4320dc5..11d27feab0b 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -3,6 +3,7 @@ require 'tempfile'
feature 'Builds', :feature do
let(:user) { create(:user) }
+ let(:user_access_level) { :developer }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -14,7 +15,7 @@ feature 'Builds', :feature do
end
before do
- project.team << [user, :developer]
+ project.team << [user, user_access_level]
login_as(user)
end
@@ -131,7 +132,9 @@ feature 'Builds', :feature do
context 'Artifacts expire date' do
before do
- build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at)
+ build.update_attributes(artifacts_file: artifacts_file,
+ artifacts_expire_at: expire_at)
+
visit namespace_project_build_path(project.namespace, project, build)
end
@@ -146,12 +149,23 @@ feature 'Builds', :feature do
context 'when expire date is defined' do
let(:expire_at) { Time.now + 7.days }
- it 'keeps artifacts when Keep button is clicked' do
- expect(page).to have_content 'The artifacts will be removed'
- click_link 'Keep'
+ context 'when user has ability to update build' do
+ it 'keeps artifacts when keep button is clicked' do
+ expect(page).to have_content 'The artifacts will be removed'
- expect(page).not_to have_link 'Keep'
- expect(page).not_to have_content 'The artifacts will be removed'
+ click_link 'Keep'
+
+ expect(page).to have_no_link 'Keep'
+ expect(page).to have_no_content 'The artifacts will be removed'
+ end
+ end
+
+ context 'when user does not have ability to update build' do
+ let(:user_access_level) { :guest }
+
+ it 'does not have keep button' do
+ expect(page).to have_no_link 'Keep'
+ 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/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/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 3ba996e2e10..ca18ac073d8 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -128,13 +128,13 @@ describe 'Pipelines', :feature, :js do
it 'has link to the manual action' do
find('.js-pipeline-dropdown-manual-actions').click
- expect(page).to have_link('Manual build')
+ expect(page).to have_link('manual build')
end
context 'when manual action was played' do
before do
find('.js-pipeline-dropdown-manual-actions').click
- click_link('Manual build')
+ click_link('manual build')
end
it 'enqueues manual action job' do
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index bf60cca4ea4..55d5d082c6e 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -21,6 +21,16 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_button 'Save changes'
end
+
+ scenario 'shows a successful notice when the project is updated' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'project_name_edit', with: 'hello world'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content "Project 'hello world' was successfully updated."
+ end
end
describe 'Rename repository' do
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index 8de827447ff..86a07b2c679 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -33,10 +33,89 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to eq(token)
end
- describe 'mattermost service is enabled' do
- it 'shows the add to mattermost button' do
- expect(page).to have_link 'Add to Mattermost'
+ it 'shows the add to mattermost button' do
+ expect(page).to have_link('Add to Mattermost')
+ end
+
+ it 'shows an explanation if user is a member of no teams' do
+ stub_teams(count: 0)
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_content('You aren’t a member of any team on the Mattermost instance')
+ expect(page).to have_link('join a team', href: "#{Gitlab.config.mattermost.host}/select_team")
+ end
+
+ it 'shows an explanation if user is a member of 1 team' do
+ stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_content('The team where the slash commands will be used in')
+ expect(page).to have_content('This is the only available team.')
+ end
+
+ it 'shows a disabled prefilled select if user is a member of 1 team' do
+ teams = stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ team_name = teams.first[1]['display_name']
+ select_element = find('select#mattermost_team_id')
+ selected_option = select_element.find('option[selected]')
+
+ expect(select_element['disabled']).to be(true)
+ expect(selected_option).to have_content(team_name.to_s)
+ end
+
+ it 'has a hidden input for the prefilled value if user is a member of 1 team' do
+ teams = stub_teams(count: 1)
+
+ click_link 'Add to Mattermost'
+
+ expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s)
+ end
+
+ it 'shows an explanation user is a member of multiple teams' do
+ stub_teams(count: 2)
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_content('Select the team where the slash commands will be used in')
+ expect(page).to have_content('The list shows all available teams.')
+ end
+
+ it 'shows a select with team options user is a member of multiple teams' do
+ stub_teams(count: 2)
+
+ click_link 'Add to Mattermost'
+
+ select_element = find('select#mattermost_team_id')
+ selected_option = select_element.find('option[selected]')
+
+ expect(select_element['disabled']).to be(false)
+ expect(selected_option).to have_content('Select team...')
+ # The 'Select team...' placeholder is item `0`.
+ expect(select_element.all('option').count).to eq(3)
+ end
+
+ def stub_teams(count: 0)
+ teams = create_teams(count)
+
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams }
+
+ teams
+ end
+
+ def create_teams(count = 0)
+ teams = {}
+
+ count.times do |i|
+ i += 1
+ teams[i] = { id: i, display_name: i }
end
+
+ teams
end
describe 'mattermost service is not enabled' do
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 caecd027aaa..a05b83959fb 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -169,16 +169,16 @@ describe "Search", feature: true do
find('.dropdown-menu').click_link 'Issues assigned to me'
sleep 2
- expect(page).to have_selector('.issues-holder')
- expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
end
it 'takes user to her issues page when issues authored is clicked' do
find('.dropdown-menu').click_link "Issues I've created"
sleep 2
- expect(page).to have_selector('.issues-holder')
- expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
+ expect(page).to have_selector('.filtered-search')
+ expect(find('.filtered-search').value).to eq("author:@#{user.username}")
end
it 'takes user to her MR page when MR assigned is clicked' do
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/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb
index cb95e7828db..5470276bf06 100644
--- a/spec/features/snippets/create_snippet_spec.rb
+++ b/spec/features/snippets/create_snippet_spec.rb
@@ -17,4 +17,18 @@ feature 'Create Snippet', feature: true do
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('Hello World!')
end
+
+ scenario 'Authenticated user creates a snippet with + in filename' do
+ fill_in 'personal_snippet_title', with: 'My Snippet Title'
+ page.within('.file-editor') do
+ find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
+ find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!'
+ end
+
+ click_button 'Create snippet'
+
+ expect(page).to have_content('My Snippet Title')
+ expect(page).to have_content('snippet+file+name')
+ expect(page).to have_content('Hello World!')
+ end
end
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/fixtures/config/redis_config_with_env.yml b/spec/fixtures/config/redis_config_with_env.yml
new file mode 100644
index 00000000000..f5860f37e47
--- /dev/null
+++ b/spec/fixtures/config/redis_config_with_env.yml
@@ -0,0 +1,2 @@
+test:
+ url: <%= ENV['TEST_GITLAB_REDIS_URL'] %>
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 903224589dd..1f221487393 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -62,4 +62,19 @@ describe MergeRequestsHelper do
it { is_expected.to eq([source_title, target_title]) }
end
end
+
+ describe 'mr_widget_refresh_url' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:project) { create(:project) }
+
+ it 'returns correct url for MR' do
+ expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
+
+ expect(mr_widget_refresh_url(merge_request)).to end_with(expected_url)
+ end
+
+ it 'returns empty string for nil' do
+ expect(mr_widget_refresh_url(nil)).to end_with('')
+ end
+ 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/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_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
new file mode 100644
index 00000000000..19bd8d53219
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
@@ -0,0 +1,134 @@
+//= require extensions/array
+//= require filtered_search/dropdown_utils
+//= require filtered_search/filtered_search_tokenizer
+//= require filtered_search/filtered_search_dropdown_manager
+
+(() => {
+ describe('Dropdown Utils', () => {
+ describe('getEscapedText', () => {
+ it('should return same word when it has no space', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
+ expect(escaped).toBe('textWithoutSpace');
+ });
+
+ it('should escape with double quotes', () => {
+ let escaped = gl.DropdownUtils.getEscapedText('text with space');
+ expect(escaped).toBe('"text with space"');
+
+ escaped = gl.DropdownUtils.getEscapedText('won\'t fix');
+ expect(escaped).toBe('"won\'t fix"');
+ });
+
+ it('should escape with single quotes', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t fix');
+ expect(escaped).toBe('\'won"t fix\'');
+ });
+
+ it('should escape with single quotes by default', () => {
+ const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix');
+ expect(escaped).toBe('\'won"t\' fix\'');
+ });
+ });
+
+ describe('filterWithSymbol', () => {
+ let input;
+ const item = {
+ title: '@root',
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
+
+ input = document.getElementById('test');
+ });
+
+ it('should filter without symbol', () => {
+ input.value = ':roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with symbol', () => {
+ input.value = '@roo';
+
+ const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with colon', () => {
+ 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', () => {
+ input.value = 'l';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ });
+ expect(updatedItem.droplab_hidden).toBe(false);
+
+ 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(input, {}, '');
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+ });
+
+ describe('setDataValueIfSelected', () => {
+ beforeEach(() => {
+ spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput')
+ .and.callFake(() => {});
+ });
+
+ it('calls addWordToInput when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
+
+ gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
+ });
+
+ it('returns true when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).toBe(true);
+ });
+
+ it('returns false when dataValue does not exist', () => {
+ const selected = {
+ getAttribute: () => null,
+ };
+
+ const result = gl.DropdownUtils.setDataValueIfSelected(null, selected);
+ expect(result).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
new file mode 100644
index 00000000000..d0d27ceb4a6
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
@@ -0,0 +1,59 @@
+//= require extensions/array
+//= require filtered_search/filtered_search_tokenizer
+//= require filtered_search/filtered_search_dropdown_manager
+
+(() => {
+ describe('Filtered Search Dropdown Manager', () => {
+ describe('addWordToInput', () => {
+ function getInputValue() {
+ return document.querySelector('.filtered-search').value;
+ }
+
+ function setInputValue(value) {
+ document.querySelector('.filtered-search').value = value;
+ }
+
+ beforeEach(() => {
+ const input = document.createElement('input');
+ input.classList.add('filtered-search');
+ document.body.appendChild(input);
+ });
+
+ afterEach(() => {
+ document.querySelector('.filtered-search').outerHTML = '';
+ });
+
+ describe('input has no existing value', () => {
+ it('should add just tokenName', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('milestone');
+ expect(getInputValue()).toBe('milestone:');
+ });
+
+ it('should add tokenName and tokenValue', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
+ expect(getInputValue()).toBe('label:none');
+ });
+ });
+
+ describe('input has existing value', () => {
+ it('should be able to just add tokenName', () => {
+ setInputValue('a');
+ gl.FilteredSearchDropdownManager.addWordToInput('author');
+ expect(getInputValue()).toBe('author:');
+ });
+
+ it('should replace tokenValue', () => {
+ setInputValue('author:roo');
+ gl.FilteredSearchDropdownManager.addWordToInput('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"\'');
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
new file mode 100644
index 00000000000..6df7c0e44ef
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
@@ -0,0 +1,104 @@
+//= require extensions/array
+//= require filtered_search/filtered_search_token_keys
+
+(() => {
+ describe('Filtered Search Token Keys', () => {
+ describe('get', () => {
+ let tokenKeys;
+
+ beforeEach(() => {
+ tokenKeys = gl.FilteredSearchTokenKeys.get();
+ });
+
+ it('should return tokenKeys', () => {
+ expect(tokenKeys !== null).toBe(true);
+ });
+
+ it('should return tokenKeys as an array', () => {
+ expect(tokenKeys instanceof Array).toBe(true);
+ });
+ });
+
+ describe('getConditions', () => {
+ let conditions;
+
+ beforeEach(() => {
+ conditions = gl.FilteredSearchTokenKeys.getConditions();
+ });
+
+ it('should return conditions', () => {
+ expect(conditions !== null).toBe(true);
+ });
+
+ it('should return conditions as an array', () => {
+ expect(conditions instanceof Array).toBe(true);
+ });
+ });
+
+ describe('searchByKey', () => {
+ it('should return null when key not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchBySymbol', () => {
+ it('should return null when symbol not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by symbol', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByKeyParam', () => {
+ it('should return null when key param not found', () => {
+ const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam');
+ expect(tokenKey === null).toBe(true);
+ });
+
+ it('should return tokenKey when found by key param', () => {
+ const tokenKeys = gl.FilteredSearchTokenKeys.get();
+ const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`);
+ expect(result).toEqual(tokenKeys[0]);
+ });
+ });
+
+ describe('searchByConditionUrl', () => {
+ it('should return null when condition url not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by url', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url);
+ expect(result).toBe(conditions[0]);
+ });
+ });
+
+ describe('searchByConditionKeyValue', () => {
+ it('should return null when condition tokenKey and value not found', () => {
+ const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null);
+ expect(condition === null).toBe(true);
+ });
+
+ it('should return condition when found by tokenKey and value', () => {
+ const conditions = gl.FilteredSearchTokenKeys.getConditions();
+ const result = gl.FilteredSearchTokenKeys
+ .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value);
+ expect(result).toEqual(conditions[0]);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
new file mode 100644
index 00000000000..ac7f8e9cbcd
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
@@ -0,0 +1,104 @@
+//= require extensions/array
+//= require filtered_search/filtered_search_token_keys
+//= require filtered_search/filtered_search_tokenizer
+
+(() => {
+ describe('Filtered Search Tokenizer', () => {
+ describe('processTokens', () => {
+ it('returns for input containing only search value', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('searchTerm');
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(0);
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing only tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none');
+ expect(results.searchToken).toBe('');
+ expect(results.tokens.length).toBe(4);
+ expect(results.tokens[3]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Very Important"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('v1.0');
+ expect(results.tokens[2].symbol).toBe('%');
+
+ expect(results.tokens[3].key).toBe('assignee');
+ expect(results.tokens[3].value).toBe('none');
+ expect(results.tokens[3].symbol).toBe('');
+ });
+
+ it('returns for input starting with search value and ending with tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('searchTerm anotherSearchTerm milestone:none');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0]).toBe(results.lastToken);
+ expect(results.tokens[0].key).toBe('milestone');
+ expect(results.tokens[0].value).toBe('none');
+ expect(results.tokens[0].symbol).toBe('');
+ });
+
+ it('returns for input starting with tokens and ending with search value', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('assignee:@user searchTerm');
+
+ expect(results.searchToken).toBe('searchTerm');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0].key).toBe('assignee');
+ expect(results.tokens[0].value).toBe('user');
+ expect(results.tokens[0].symbol).toBe('@');
+ expect(results.lastToken).toBe(results.searchToken);
+ });
+
+ it('returns for input containing search value wrapped between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none');
+
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('label');
+ expect(results.tokens[1].value).toBe('"Won\'t fix"');
+ expect(results.tokens[1].symbol).toBe('~');
+
+ expect(results.tokens[2].key).toBe('milestone');
+ expect(results.tokens[2].value).toBe('none');
+ expect(results.tokens[2].symbol).toBe('');
+ });
+
+ it('returns for input containing search value in between tokens', () => {
+ const results = gl.FilteredSearchTokenizer
+ .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing');
+ expect(results.searchToken).toBe('searchTerm anotherSearchTerm');
+ expect(results.tokens.length).toBe(3);
+ expect(results.tokens[2]).toBe(results.lastToken);
+
+ expect(results.tokens[0].key).toBe('author');
+ expect(results.tokens[0].value).toBe('root');
+ expect(results.tokens[0].symbol).toBe('@');
+
+ expect(results.tokens[1].key).toBe('assignee');
+ expect(results.tokens[1].value).toBe('none');
+ expect(results.tokens[1].symbol).toBe('');
+
+ expect(results.tokens[2].key).toBe('label');
+ expect(results.tokens[2].value).toBe('Doing');
+ expect(results.tokens[2].symbol).toBe('~');
+ });
+ });
+ });
+})();
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 ce96571bd52..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');
@@ -50,6 +50,9 @@
selectable: true,
filterable: isFilterable,
data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+ search: {
+ fields: ['name']
+ },
text: (project) => {
(project.name_with_namespace || project.name);
},
@@ -86,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');
@@ -97,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');
@@ -106,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);
});
});
@@ -137,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',
@@ -161,11 +164,26 @@
});
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', () => {
+ const $searchInput = $(SEARCH_INPUT_SELECTOR);
+ initDropDown.call(this, false, true);
+ $searchInput
+ .trigger('focus')
+ .val('g')
+ .trigger('input');
+ expect($searchInput.val()).toEqual('g');
+ this.dropdownButtonElement.trigger('hidden.bs.dropdown');
+ $searchInput
+ .trigger('blur')
+ .trigger('focus');
+ expect($searchInput.val()).toEqual('g');
+ });
});
})();
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 ef75f600898..031f9ca03c9 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -15,6 +15,7 @@
expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22');
});
});
+
describe('gl.utils.parseUrlPathname', () => {
beforeEach(() => {
spyOn(gl.utils, 'parseUrl').and.callFake(url => ({
@@ -28,5 +29,28 @@
expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
});
});
+
+ describe('gl.utils.getUrlParamsArray', () => {
+ it('should return params array', () => {
+ expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true);
+ });
+
+ it('should remove the question mark from the search params', () => {
+ const paramsArray = gl.utils.getUrlParamsArray();
+ expect(paramsArray[0][0] !== '?').toBe(true);
+ });
+ });
+
+ describe('gl.utils.getParameterByName', () => {
+ it('should return valid parameter', () => {
+ const value = gl.utils.getParameterByName('reporter');
+ expect(value).toBe('Console');
+ });
+
+ it('should return invalid parameter', () => {
+ const value = gl.utils.getParameterByName('fakeParameter');
+ expect(value).toBe(null);
+ });
+ });
});
})();
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6
new file mode 100644
index 00000000000..e97356b65d5
--- /dev/null
+++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6
@@ -0,0 +1,25 @@
+//= require lib/utils/text_utility
+
+(() => {
+ describe('text_utility', () => {
+ describe('gl.text.getTextWidth', () => {
+ it('returns zero width when no text is passed', () => {
+ expect(gl.text.getTextWidth('')).toBe(0);
+ });
+
+ it('returns zero width when no text is passed and font is passed', () => {
+ expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
+ });
+
+ it('returns width when text is passed', () => {
+ expect(gl.text.getTextWidth('foo') > 0).toBe(true);
+ });
+
+ it('returns bigger width when font is larger', () => {
+ const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
+ const regular = gl.text.getTextWidth('foo', '10px sans-serif');
+ expect(largeFont > regular).toBe(true);
+ });
+ });
+ });
+})();
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 e13c4ad772c..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 */
@@ -11,6 +11,7 @@
(function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
+ var userName = 'root';
widget = null;
@@ -19,6 +20,7 @@
window.gon || (window.gon = {});
window.gon.current_user_id = userId;
+ window.gon.current_username = userName;
dashboardIssuesPath = '/dashboard/issues';
@@ -93,8 +95,8 @@
assertLinks = function(list, issuesPath, mrsPath) {
var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
- issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId;
- issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId;
+ issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName;
+ issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName;
mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId;
mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId;
a1 = "a[href='" + issuesAssignedToMeLink + "']";
@@ -169,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/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 62d68721574..f824e2e1efe 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -769,6 +769,19 @@ module Ci
expect(builds.first[:environment]).to eq(environment[:name])
expect(builds.first[:options]).to include(environment: environment)
end
+
+ context 'the url has a port as variable' do
+ let(:environment) do
+ { name: 'production',
+ url: 'http://production.gitlab.com:$PORT' }
+ end
+
+ it 'allows a variable for the port' do
+ expect(builds.size).to eq(1)
+ expect(builds.first[:environment]).to eq(environment[:name])
+ expect(builds.first[:options]).to include(environment: environment)
+ end
+ end
end
context 'when no environment is specified' do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index f3843ca64ff..ba199917f5c 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -8,6 +8,10 @@ module Gitlab
let(:html) { 'H<sub>2</sub>O' }
context "without project" do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults)
+ end
+
it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index 39069b49978..98effecdbbc 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -56,7 +56,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
- expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.')
@@ -88,8 +87,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
end
it 'returns an error if the user is not allowed to delete protected branches' do
- expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false)
-
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to delete protected branches from this project.')
end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index d97806295fb..2adbed2154f 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -196,22 +196,5 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
end
-
- context 'when invalid URL is used' do
- let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } }
-
- describe '#valid?' do
- it 'is not valid' do
- expect(entry).not_to be_valid
- end
- end
-
- describe '#errors?' do
- it 'contains error about invalid URL' do
- expect(entry.errors)
- .to include "environment url must be a valid url"
- 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/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/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/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index 7371b578a48..fb470ea7568 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -126,5 +126,16 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
end
+
+ it 'does not tag a transaction if route infos are missing' do
+ endpoint = double(:endpoint)
+ allow(endpoint).to receive(:route).and_raise
+
+ env['api.endpoint'] = endpoint
+
+ middleware.tag_endpoint(transaction, env)
+
+ expect(transaction.action).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index e5406fb2d33..917c5c46db1 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Redis do
- let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
+ include StubENV
before(:each) { clear_raw_config }
after(:each) { clear_raw_config }
@@ -72,6 +72,20 @@ describe Gitlab::Redis do
expect(url2).not_to end_with('foobar')
end
+
+ context 'when yml file with env variable' do
+ let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') }
+
+ before do
+ stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379')
+ end
+
+ it 'reads redis url from env variable' do
+ stub_const("#{described_class}::CONFIG_FILE", redis_config)
+
+ expect(described_class.url).to eq 'redis://redishost:6379'
+ end
+ end
end
describe '._raw_config' 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/migrations/fill_authorized_projects_spec.rb b/spec/migrations/fill_authorized_projects_spec.rb
new file mode 100644
index 00000000000..99dc4195818
--- /dev/null
+++ b/spec/migrations/fill_authorized_projects_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170106142508_fill_authorized_projects.rb')
+
+describe FillAuthorizedProjects do
+ describe '#up' do
+ it 'schedules the jobs in batches' do
+ user1 = create(:user)
+ user2 = create(:user)
+
+ expect(Sidekiq::Client).to receive(:push_bulk).with(
+ 'class' => 'AuthorizedProjectsWorker',
+ 'args' => [[user1.id], [user2.id]]
+ )
+
+ described_class.new.up
+ end
+ end
+end
diff --git a/spec/migrations/remove_dot_git_from_usernames.rb b/spec/migrations/remove_dot_git_from_usernames.rb
deleted file mode 100644
index 1b1d2adc463..00000000000
--- a/spec/migrations/remove_dot_git_from_usernames.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# encoding: utf-8
-
-require 'spec_helper'
-require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb')
-
-describe RemoveDotGitFromUsernames do
- let(:user) { create(:user) }
-
- describe '#up' do
- let(:migration) { described_class.new }
-
- before do
- namespace = user.namespace
- namespace.path = 'test.git'
- namespace.save!(validate: false)
-
- user.username = 'test.git'
- user.save!(validate: false)
- end
-
- it 'renames user with .git in username' do
- migration.up
-
- expect(user.reload.username).to eq('test_git')
- expect(user.namespace.reload.path).to eq('test_git')
- expect(user.namespace.route.path).to eq('test_git')
- end
- end
-end
diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb
new file mode 100644
index 00000000000..8737e00eaeb
--- /dev/null
+++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb
@@ -0,0 +1,57 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb')
+
+describe RemoveDotGitFromUsernames do
+ let(:user) { create(:user) }
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ before do
+ update_namespace(user, 'test.git')
+ end
+
+ it 'renames user with .git in username' do
+ migration.up
+
+ expect(user.reload.username).to eq('test_git')
+ expect(user.namespace.reload.path).to eq('test_git')
+ expect(user.namespace.route.path).to eq('test_git')
+ end
+ end
+
+ context 'when new path exists already' do
+ describe '#up' do
+ let(:user2) { create(:user) }
+
+ before do
+ update_namespace(user, 'test.git')
+ update_namespace(user2, 'test_git')
+
+ storages = { 'default' => 'tmp/tests/custom_repositories' }
+
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ allow(migration).to receive(:route_exists?).with('test_git').and_return(true)
+ allow(migration).to receive(:route_exists?).with('test_git1').and_return(false)
+ end
+
+ it 'renames user with .git in username' do
+ migration.up
+
+ expect(user.reload.username).to eq('test_git1')
+ expect(user.namespace.reload.path).to eq('test_git1')
+ expect(user.namespace.route.path).to eq('test_git1')
+ end
+ end
+ end
+
+ def update_namespace(user, path)
+ namespace = user.namespace
+ namespace.path = path
+ namespace.save!(validate: false)
+
+ user.username = path
+ user.save!(validate: false)
+ end
+end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
deleted file mode 100644
index cd3b6d51545..00000000000
--- a/spec/models/build_spec.rb
+++ /dev/null
@@ -1,1338 +0,0 @@
-require 'spec_helper'
-
-describe Ci::Build, models: true do
- let(:project) { create(:project) }
-
- let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: project.default_branch,
- status: 'success')
- end
-
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- it { is_expected.to validate_presence_of :ref }
-
- it { is_expected.to respond_to :trace_html }
-
- describe '#first_pending' do
- let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
- let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
- subject { Ci::Build.first_pending }
-
- it { is_expected.to be_a(Ci::Build) }
- it('returns with the first pending build') { is_expected.to eq(first) }
- end
-
- describe '#create_from' do
- before do
- build.status = 'success'
- build.save
- end
- let(:create_from_build) { Ci::Build.create_from build }
-
- it 'exists a pending task' do
- expect(Ci::Build.pending.count(:all)).to eq 0
- create_from_build
- expect(Ci::Build.pending.count(:all)).to be > 0
- end
- end
-
- describe '#failed_but_allowed?' do
- subject { build.failed_but_allowed? }
-
- context 'when build is not allowed to fail' do
- before do
- build.allow_failure = false
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build.status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when build is allowed to fail' do
- before do
- build.allow_failure = true
- end
-
- context 'and build.status is success' do
- before do
- build.status = 'success'
- end
-
- it { is_expected.to be_falsey }
- end
-
- context 'and build.status is failed' do
- before do
- build.status = 'failed'
- end
-
- it { is_expected.to be_truthy }
- end
- end
- end
-
- describe '#persisted_environment' do
- before do
- @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
- end
-
- subject { build.persisted_environment }
-
- context 'referenced literally' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
-
- it { is_expected.to eq(@environment) }
- end
-
- context 'referenced with a variable' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
-
- it { is_expected.to eq(@environment) }
- end
- end
-
- describe '#trace' do
- it { expect(build.trace).to be_nil }
-
- context 'when build.trace contains text' do
- let(:text) { 'example output' }
- before do
- build.trace = text
- end
-
- it { expect(build.trace).to eq(text) }
- end
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(trace: token)
- build.project.update(runners_token: token)
- end
-
- it { expect(build.trace).not_to include(token) }
- it { expect(build.raw_trace).to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(trace: token)
- build.update(token: token)
- end
-
- it { expect(build.trace).not_to include(token) }
- it { expect(build.raw_trace).to include(token) }
- end
- end
-
- describe '#raw_trace' do
- subject { build.raw_trace }
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- build.update(trace: token)
- end
-
- it { is_expected.not_to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- build.update(trace: token)
- end
-
- it { is_expected.not_to include(token) }
- end
- end
-
- context '#append_trace' do
- subject { build.trace_html }
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- build.append_trace(token, 0)
- end
-
- it { is_expected.not_to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- build.append_trace(token, 0)
- end
-
- it { is_expected.not_to include(token) }
- end
- end
-
- # TODO: build timeout
- # describe :timeout do
- # subject { build.timeout }
- #
- # it { is_expected.to eq(pipeline.project.timeout) }
- # end
-
- describe '#options' do
- let(:options) do
- {
- image: "ruby:2.1",
- services: [
- "postgres"
- ]
- }
- end
-
- subject { build.options }
- it { is_expected.to eq(options) }
- end
-
- # TODO: allow_git_fetch
- # describe :allow_git_fetch do
- # subject { build.allow_git_fetch }
- #
- # it { is_expected.to eq(project.allow_git_fetch) }
- # end
-
- describe '#project' do
- subject { build.project }
-
- it { is_expected.to eq(pipeline.project) }
- end
-
- describe '#project_id' do
- subject { build.project_id }
-
- it { is_expected.to eq(pipeline.project_id) }
- end
-
- describe '#project_name' do
- subject { build.project_name }
-
- it { is_expected.to eq(project.name) }
- end
-
- describe '#extract_coverage' do
- context 'valid content & regex' do
- subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to eq(98.29) }
- end
-
- context 'valid content & bad regex' do
- subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') }
-
- it { is_expected.to be_nil }
- end
-
- context 'no coverage content & regex' do
- subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to be_nil }
- end
-
- context 'multiple results in content & regex' do
- subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to eq(98.29) }
- end
-
- context 'using a regex capture' do
- subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') }
-
- it { is_expected.to eq(65) }
- end
- end
-
- describe '#ref_slug' do
- {
- 'master' => 'master',
- '1-foo' => '1-foo',
- 'fix/1-foo' => 'fix-1-foo',
- 'fix-1-foo' => 'fix-1-foo',
- 'a' * 63 => 'a' * 63,
- 'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
- }.each do |ref, slug|
- it "transforms #{ref} to #{slug}" do
- build.ref = ref
-
- expect(build.ref_slug).to eq(slug)
- end
- end
- end
-
- describe '#variables' do
- let(:container_registry_enabled) { false }
- let(:predefined_variables) do
- [
- { key: 'CI', value: 'true', public: true },
- { key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
- { key: 'CI_BUILD_REF', value: build.sha, public: true },
- { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
- { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
- { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
- { key: 'CI_BUILD_NAME', value: 'test', public: true },
- { key: 'CI_BUILD_STAGE', value: 'test', public: true },
- { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
- { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
- { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
- { key: 'CI_PROJECT_NAME', value: project.path, public: true },
- { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
- { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
- { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
- { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
- ]
- end
-
- before do
- stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com')
- end
-
- subject { build.variables }
-
- context 'returns variables' do
- before do
- build.yaml_variables = []
- end
-
- it { is_expected.to eq(predefined_variables) }
- end
-
- context 'when build has user' do
- let(:user) { create(:user, username: 'starter') }
- let(:user_variables) do
- [
- { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
- { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
- ]
- end
-
- before do
- build.update_attributes(user: user)
- end
-
- it { user_variables.each { |v| is_expected.to include(v) } }
- end
-
- context 'when build has an environment' do
- before do
- build.update(environment: 'production')
- create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
- end
-
- let(:environment_variables) do
- [
- { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
- { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
- ]
- end
-
- it { environment_variables.each { |v| is_expected.to include(v) } }
- end
-
- context 'when build started manually' do
- before do
- build.update_attributes(when: :manual)
- end
-
- let(:manual_variable) do
- { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
- end
-
- it { is_expected.to include(manual_variable) }
- end
-
- context 'when build is for tag' do
- let(:tag_variable) do
- { key: 'CI_BUILD_TAG', value: 'master', public: true }
- end
-
- before do
- build.update_attributes(tag: true)
- end
-
- it { is_expected.to include(tag_variable) }
- end
-
- context 'when secure variable is defined' do
- let(:secure_variable) do
- { key: 'SECRET_KEY', value: 'secret_value', public: false }
- end
-
- before do
- build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
- end
-
- it { is_expected.to include(secure_variable) }
- end
-
- context 'when build is for triggers' do
- let(:trigger) { create(:ci_trigger, project: project) }
- let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
- let(:user_trigger_variable) do
- { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
- end
- let(:predefined_trigger_variable) do
- { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
- end
-
- before do
- build.trigger_request = trigger_request
- end
-
- it { is_expected.to include(user_trigger_variable) }
- it { is_expected.to include(predefined_trigger_variable) }
- end
-
- context 'when yaml_variables are undefined' do
- before do
- build.yaml_variables = nil
- end
-
- context 'use from gitlab-ci.yml' do
- before do
- stub_ci_pipeline_yaml_file(config)
- end
-
- context 'when config is not found' do
- let(:config) { nil }
-
- it { is_expected.to eq(predefined_variables) }
- end
-
- context 'when config does not have a questioned job' do
- let(:config) do
- YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
- end
-
- it { is_expected.to eq(predefined_variables) }
- end
-
- context 'when config has variables' do
- let(:config) do
- YAML.dump({
- test: {
- script: 'Hello World',
- variables: {
- KEY: 'value'
- }
- }
- })
- end
- let(:variables) do
- [{ key: 'KEY', value: 'value', public: true }]
- end
-
- it { is_expected.to eq(predefined_variables + variables) }
- end
- end
- end
-
- context 'when container registry is enabled' do
- let(:container_registry_enabled) { true }
- let(:ci_registry) do
- { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
- end
- let(:ci_registry_image) do
- { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
- end
-
- context 'and is disabled for project' do
- before do
- project.update(container_registry_enabled: false)
- end
-
- it { is_expected.to include(ci_registry) }
- it { is_expected.not_to include(ci_registry_image) }
- end
-
- context 'and is enabled for project' do
- before do
- project.update(container_registry_enabled: true)
- end
-
- it { is_expected.to include(ci_registry) }
- it { is_expected.to include(ci_registry_image) }
- end
- end
-
- context 'when runner is assigned to build' do
- let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
-
- before do
- build.update(runner: runner)
- end
-
- it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) }
- it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) }
- it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
- end
-
- context 'when build is for a deployment' do
- let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
-
- before do
- build.environment = 'production'
- allow(project).to receive(:deployment_variables).and_return([deployment_variable])
- end
-
- it { is_expected.to include(deployment_variable) }
- end
-
- context 'returns variables in valid order' do
- before do
- allow(build).to receive(:predefined_variables) { ['predefined'] }
- allow(project).to receive(:predefined_variables) { ['project'] }
- allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
- allow(build).to receive(:yaml_variables) { ['yaml'] }
- allow(project).to receive(:secret_variables) { ['secret'] }
- end
-
- it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
- end
- end
-
- describe '#has_tags?' do
- context 'when build has tags' do
- subject { create(:ci_build, tag_list: ['tag']) }
- it { is_expected.to have_tags }
- end
-
- context 'when build does not have tags' do
- subject { create(:ci_build, tag_list: []) }
- it { is_expected.not_to have_tags }
- end
- end
-
- describe '#any_runners_online?' do
- subject { build.any_runners_online? }
-
- context 'when no runners' do
- it { is_expected.to be_falsey }
- end
-
- context 'when there are runners' do
- let(:runner) { create(:ci_runner) }
-
- before do
- build.project.runners << runner
- runner.update_attributes(contacted_at: 1.second.ago)
- end
-
- it { is_expected.to be_truthy }
-
- it 'that is inactive' do
- runner.update_attributes(active: false)
- is_expected.to be_falsey
- end
-
- it 'that is not online' do
- runner.update_attributes(contacted_at: nil)
- is_expected.to be_falsey
- end
-
- it 'that cannot handle build' do
- expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
- is_expected.to be_falsey
- end
- end
- end
-
- describe '#stuck?' do
- subject { build.stuck? }
-
- context "when commit_status.status is pending" do
- before do
- build.status = 'pending'
- end
-
- it { is_expected.to be_truthy }
-
- context "and there are specific runner" do
- let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
-
- before do
- build.project.runners << runner
- runner.save
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- %w[success failed canceled running].each do |state|
- context "when commit_status.status is #{state}" do
- before do
- build.status = state
- end
-
- it { is_expected.to be_falsey }
- end
- end
- end
-
- describe '#artifacts?' do
- subject { build.artifacts? }
-
- context 'artifacts archive does not exist' do
- before do
- build.update_attributes(artifacts_file: nil)
- end
-
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
- it { is_expected.to be_truthy }
-
- context 'is expired' do
- before { build.update(artifacts_expire_at: Time.now - 7.days) }
- it { is_expected.to be_falsy }
- end
-
- context 'is not expired' do
- before { build.update(artifacts_expire_at: Time.now + 7.days) }
- it { is_expected.to be_truthy }
- end
- end
- end
-
- describe '#artifacts_expired?' do
- subject { build.artifacts_expired? }
-
- context 'is expired' do
- before { build.update(artifacts_expire_at: Time.now - 7.days) }
-
- it { is_expected.to be_truthy }
- end
-
- context 'is not expired' do
- before { build.update(artifacts_expire_at: Time.now + 7.days) }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#artifacts_metadata?' do
- subject { build.artifacts_metadata? }
- context 'artifacts metadata does not exist' do
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive is a zip file and metadata exists' do
- let(:build) { create(:ci_build, :artifacts) }
- it { is_expected.to be_truthy }
- end
- end
- describe '#repo_url' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
-
- subject { build.repo_url }
-
- it { is_expected.to be_a(String) }
- it { is_expected.to end_with(".git") }
- it { is_expected.to start_with(project.web_url[0..6]) }
- it { is_expected.to include(build.token) }
- it { is_expected.to include('gitlab-ci-token') }
- it { is_expected.to include(project.web_url[7..-1]) }
- end
-
- describe '#artifacts_expire_in' do
- subject { build.artifacts_expire_in }
- it { is_expected.to be_nil }
-
- context 'when artifacts_expire_at is specified' do
- let(:expire_at) { Time.now + 7.days }
-
- before { build.artifacts_expire_at = expire_at }
-
- it { is_expected.to be_within(5).of(expire_at - Time.now) }
- end
- end
-
- describe '#artifacts_expire_in=' do
- subject { build.artifacts_expire_in }
-
- it 'when assigning valid duration' do
- build.artifacts_expire_in = '7 days'
-
- is_expected.to be_within(10).of(7.days.to_i)
- end
-
- it 'when assigning invalid duration' do
- expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
- is_expected.to be_nil
- end
-
- it 'when resseting value' do
- build.artifacts_expire_in = nil
-
- is_expected.to be_nil
- end
- end
-
- describe '#keep_artifacts!' do
- let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
-
- it 'to reset expire_at' do
- build.keep_artifacts!
-
- expect(build.artifacts_expire_at).to be_nil
- end
- end
-
- describe '#depends_on_builds' do
- let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
- let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
- let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
- let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
-
- it 'expects to have no dependents if this is first build' do
- expect(build.depends_on_builds).to be_empty
- end
-
- it 'expects to have one dependent if this is test' do
- expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
- end
-
- it 'expects to have all builds from build and test stage if this is last' do
- expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
- end
-
- it 'expects to have retried builds instead the original ones' do
- retried_rspec = Ci::Build.retry(rspec_test)
- expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
- end
- end
-
- def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
- create(factory, source_project_id: pipeline.gl_project_id,
- target_project_id: pipeline.gl_project_id,
- source_branch: build.ref,
- created_at: created_at)
- end
-
- describe '#merge_request' do
- context 'when a MR has a reference to the pipeline' do
- before do
- @merge_request = create_mr(build, pipeline, factory: :merge_request)
-
- commits = [double(id: pipeline.sha)]
- allow(@merge_request).to receive(:commits).and_return(commits)
- allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
- end
-
- it 'returns the single associated MR' do
- expect(build.merge_request.id).to eq(@merge_request.id)
- end
- end
-
- context 'when there is not a MR referencing the pipeline' do
- it 'returns nil' do
- expect(build.merge_request).to be_nil
- end
- end
-
- context 'when more than one MR have a reference to the pipeline' do
- before do
- @merge_request = create_mr(build, pipeline, factory: :merge_request)
- @merge_request.close!
- @merge_request2 = create_mr(build, pipeline, factory: :merge_request)
-
- commits = [double(id: pipeline.sha)]
- allow(@merge_request).to receive(:commits).and_return(commits)
- allow(@merge_request2).to receive(:commits).and_return(commits)
- allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2])
- end
-
- it 'returns the first MR' do
- expect(build.merge_request.id).to eq(@merge_request.id)
- end
- end
-
- context 'when a Build is created after the MR' do
- before do
- @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs)
- pipeline2 = create(:ci_pipeline, project: project)
- @build2 = create(:ci_build, pipeline: pipeline2)
-
- allow(@merge_request).to receive(:commits_sha).
- and_return([pipeline.sha, pipeline2.sha])
- allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
- end
-
- it 'returns the current MR' do
- expect(@build2.merge_request.id).to eq(@merge_request.id)
- end
- end
- end
-
- describe 'build erasable' do
- shared_examples 'erasable' do
- it 'removes artifact file' do
- expect(build.artifacts_file.exists?).to be_falsy
- end
-
- it 'removes artifact metadata file' do
- expect(build.artifacts_metadata.exists?).to be_falsy
- end
-
- it 'erases build trace in trace file' do
- expect(build.trace).to be_empty
- end
-
- it 'sets erased to true' do
- expect(build.erased?).to be true
- end
-
- it 'sets erase date' do
- expect(build.erased_at).not_to be_falsy
- end
- end
-
- context 'build is not erasable' do
- let!(:build) { create(:ci_build) }
-
- describe '#erase' do
- subject { build.erase }
-
- it { is_expected.to be false }
- end
-
- describe '#erasable?' do
- subject { build.erasable? }
- it { is_expected.to eq false }
- end
- end
-
- context 'build is erasable' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
-
- describe '#erase' do
- before do
- build.erase(erased_by: user)
- end
-
- context 'erased by user' do
- let!(:user) { create(:user, username: 'eraser') }
-
- include_examples 'erasable'
-
- it 'records user who erased a build' do
- expect(build.erased_by).to eq user
- end
- end
-
- context 'erased by system' do
- let(:user) { nil }
-
- include_examples 'erasable'
-
- it 'does not set user who erased a build' do
- expect(build.erased_by).to be_nil
- end
- end
- end
-
- describe '#erasable?' do
- subject { build.erasable? }
- it { is_expected.to be_truthy }
- end
-
- describe '#erased?' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
- subject { build.erased? }
-
- context 'build has not been erased' do
- it { is_expected.to be_falsey }
- end
-
- context 'build has been erased' do
- before do
- build.erase
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'metadata and build trace are not available' do
- let!(:build) { create(:ci_build, :success, :artifacts) }
-
- before do
- build.remove_artifacts_metadata!
- end
-
- describe '#erase' do
- it 'does not raise error' do
- expect { build.erase }.not_to raise_error
- end
- end
- end
- end
- end
-
- describe '#commit' do
- it 'returns commit pipeline has been created for' do
- expect(build.commit).to eq project.commit
- end
- end
-
- describe '#when' do
- subject { build.when }
-
- context 'when `when` is undefined' do
- before do
- build.when = nil
- end
-
- context 'use from gitlab-ci.yml' do
- before do
- stub_ci_pipeline_yaml_file(config)
- end
-
- context 'when config is not found' do
- let(:config) { nil }
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config does not have a questioned job' do
- let(:config) do
- YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
- end
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config has `when`' do
- let(:config) do
- YAML.dump({
- test: {
- script: 'Hello World',
- when: 'always'
- }
- })
- end
-
- it { is_expected.to eq('always') }
- end
- end
- end
- end
-
- describe '#cancelable?' do
- subject { build }
-
- context 'when build is cancelable' do
- context 'when build is pending' do
- it { is_expected.to be_cancelable }
- end
-
- context 'when build is running' do
- before do
- build.run!
- end
-
- it { is_expected.to be_cancelable }
- end
- end
-
- context 'when build is not cancelable' do
- context 'when build is successful' do
- before do
- build.success!
- end
-
- it { is_expected.not_to be_cancelable }
- end
-
- context 'when build is failed' do
- before do
- build.drop!
- end
-
- it { is_expected.not_to be_cancelable }
- end
- end
- end
-
- describe '#retryable?' do
- subject { build }
-
- context 'when build is retryable' do
- context 'when build is successful' do
- before do
- build.success!
- end
-
- it { is_expected.to be_retryable }
- end
-
- context 'when build is failed' do
- before do
- build.drop!
- end
-
- it { is_expected.to be_retryable }
- end
-
- context 'when build is canceled' do
- before do
- build.cancel!
- end
-
- it { is_expected.to be_retryable }
- end
- end
-
- context 'when build is not retryable' do
- context 'when build is running' do
- before do
- build.run!
- end
-
- it { is_expected.not_to be_retryable }
- end
-
- context 'when build is skipped' do
- before do
- build.skip!
- end
-
- it { is_expected.not_to be_retryable }
- end
- end
- end
-
- describe '#manual?' do
- before do
- build.update(when: value)
- end
-
- subject { build.manual? }
-
- context 'when is set to manual' do
- let(:value) { 'manual' }
-
- it { is_expected.to be_truthy }
- end
-
- context 'when set to something else' do
- let(:value) { 'something else' }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#other_actions' do
- let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
- let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
-
- subject { build.other_actions }
-
- it 'returns other actions' do
- is_expected.to contain_exactly(other_build)
- end
-
- context 'when build is retried' do
- let!(:new_build) { Ci::Build.retry(build) }
-
- it 'does not return any of them' do
- is_expected.not_to include(build, new_build)
- end
- end
-
- context 'when other build is retried' do
- let!(:retried_build) { Ci::Build.retry(other_build) }
-
- it 'returns a retried build' do
- is_expected.to contain_exactly(retried_build)
- end
- end
- end
-
- describe '#play' do
- let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
-
- subject { build.play }
-
- it 'enqueues a build' do
- is_expected.to be_pending
- is_expected.to eq(build)
- end
-
- context 'for successful build' do
- before do
- build.update(status: 'success')
- end
-
- it 'creates a new build' do
- is_expected.to be_pending
- is_expected.not_to eq(build)
- end
- end
- end
-
- describe '#when' do
- subject { build.when }
-
- context 'when `when` is undefined' do
- before do
- build.when = nil
- end
-
- context 'use from gitlab-ci.yml' do
- before do
- stub_ci_pipeline_yaml_file(config)
- end
-
- context 'when config is not found' do
- let(:config) { nil }
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config does not have a questioned job' do
- let(:config) do
- YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
- end
-
- it { is_expected.to eq('on_success') }
- end
-
- context 'when config has when' do
- let(:config) do
- YAML.dump({
- test: {
- script: 'Hello World',
- when: 'always'
- }
- })
- end
-
- it { is_expected.to eq('always') }
- end
- end
- end
- end
-
- describe '#retryable?' do
- context 'when build is running' do
- before { build.run! }
-
- it 'returns false' do
- expect(build).not_to be_retryable
- end
- end
-
- context 'when build is finished' do
- before do
- build.success!
- end
-
- it 'returns true' do
- expect(build).to be_retryable
- end
- end
- end
-
- describe '#has_environment?' do
- subject { build.has_environment? }
-
- context 'when environment is defined' do
- before do
- build.update(environment: 'review')
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when environment is not defined' do
- before do
- build.update(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#starts_environment?' do
- subject { build.starts_environment? }
-
- context 'when environment is defined' do
- before do
- build.update(environment: 'review')
- end
-
- context 'no action is defined' do
- it { is_expected.to be_truthy }
- end
-
- context 'and start action is defined' do
- before do
- build.update(options: { environment: { action: 'start' } } )
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when environment is not defined' do
- before do
- build.update(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#stops_environment?' do
- subject { build.stops_environment? }
-
- context 'when environment is defined' do
- before do
- build.update(environment: 'review')
- end
-
- context 'no action is defined' do
- it { is_expected.to be_falsey }
- end
-
- context 'and stop action is defined' do
- before do
- build.update(options: { environment: { action: 'stop' } } )
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when environment is not defined' do
- before do
- build.update(environment: nil)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#last_deployment' do
- subject { build.last_deployment }
-
- context 'when multiple deployments are created' do
- let!(:deployment1) { create(:deployment, deployable: build) }
- let!(:deployment2) { create(:deployment, deployable: build) }
-
- it 'returns the latest one' do
- is_expected.to eq(deployment2)
- end
- end
- end
-
- describe '#outdated_deployment?' do
- subject { build.outdated_deployment? }
-
- context 'when build succeeded' do
- let(:build) { create(:ci_build, :success) }
- let!(:deployment) { create(:deployment, deployable: build) }
-
- context 'current deployment is latest' do
- it { is_expected.to be_falsey }
- end
-
- context 'current deployment is not latest on environment' do
- let!(:deployment2) { create(:deployment, environment: deployment.environment) }
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'when build failed' do
- let(:build) { create(:ci_build, :failed) }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#expanded_environment_name' do
- subject { build.expanded_environment_name }
-
- context 'when environment uses $CI_BUILD_REF_NAME' do
- let(:build) do
- create(:ci_build,
- ref: 'master',
- environment: 'review/$CI_BUILD_REF_NAME')
- end
-
- it { is_expected.to eq('review/master') }
- end
-
- context 'when environment uses yaml_variables containing symbol keys' do
- let(:build) do
- create(:ci_build,
- yaml_variables: [{ key: :APP_HOST, value: 'host' }],
- environment: 'review/$APP_HOST')
- end
-
- it { is_expected.to eq('review/host') }
- end
- end
-
- describe '#detailed_status' do
- let(:user) { create(:user) }
-
- it 'returns a detailed status' do
- expect(build.detailed_status(user))
- .to be_a Gitlab::Ci::Status::Build::Cancelable
- end
- end
-end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 7e1d1126b97..f031876e812 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1,14 +1,962 @@
require 'spec_helper'
-describe Ci::Build, models: true do
- let(:build) { create(:ci_build) }
+describe Ci::Build, :models do
+ let(:project) { create(:project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
let(:test_trace) { 'This is a test' }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success')
+ end
+
it { is_expected.to belong_to(:runner) }
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
-
it { is_expected.to have_many(:deployments) }
+ it { is_expected.to validate_presence_of :ref }
+ it { is_expected.to respond_to :trace_html }
+
+ describe '#any_runners_online?' do
+ subject { build.any_runners_online? }
+
+ context 'when no runners' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when there are runners' do
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ build.project.runners << runner
+ runner.update_attributes(contacted_at: 1.second.ago)
+ end
+
+ it { is_expected.to be_truthy }
+
+ it 'that is inactive' do
+ runner.update_attributes(active: false)
+ is_expected.to be_falsey
+ end
+
+ it 'that is not online' do
+ runner.update_attributes(contacted_at: nil)
+ is_expected.to be_falsey
+ end
+
+ it 'that cannot handle build' do
+ expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
+ is_expected.to be_falsey
+ end
+ end
+ end
+
+ describe '#append_trace' do
+ subject { build.trace_html }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.append_trace(token, 0)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ build.append_trace(token, 0)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+ end
+
+ describe '#artifacts?' do
+ subject { build.artifacts? }
+
+ context 'artifacts archive does not exist' do
+ before do
+ build.update_attributes(artifacts_file: nil)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+ it { is_expected.to be_truthy }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe '#artifacts_expired?' do
+ subject { build.artifacts_expired? }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#artifacts_metadata?' do
+ subject { build.artifacts_metadata? }
+ context 'artifacts metadata does not exist' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive is a zip file and metadata exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#artifacts_expire_in' do
+ subject { build.artifacts_expire_in }
+ it { is_expected.to be_nil }
+
+ context 'when artifacts_expire_at is specified' do
+ let(:expire_at) { Time.now + 7.days }
+
+ before { build.artifacts_expire_at = expire_at }
+
+ it { is_expected.to be_within(5).of(expire_at - Time.now) }
+ end
+ end
+
+ describe '#artifacts_expire_in=' do
+ subject { build.artifacts_expire_in }
+
+ it 'when assigning valid duration' do
+ build.artifacts_expire_in = '7 days'
+
+ is_expected.to be_within(10).of(7.days.to_i)
+ end
+
+ it 'when assigning invalid duration' do
+ expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
+ is_expected.to be_nil
+ end
+
+ it 'when resseting value' do
+ build.artifacts_expire_in = nil
+
+ is_expected.to be_nil
+ end
+ end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(build.commit).to eq project.commit
+ end
+ end
+
+ describe '#create_from' do
+ before do
+ build.status = 'success'
+ build.save
+ end
+ let(:create_from_build) { Ci::Build.create_from build }
+
+ it 'exists a pending task' do
+ expect(Ci::Build.pending.count(:all)).to eq 0
+ create_from_build
+ expect(Ci::Build.pending.count(:all)).to be > 0
+ end
+ end
+
+ describe '#depends_on_builds' do
+ let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
+ let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
+ let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
+ let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
+
+ it 'expects to have no dependents if this is first build' do
+ expect(build.depends_on_builds).to be_empty
+ end
+
+ it 'expects to have one dependent if this is test' do
+ expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
+ end
+
+ it 'expects to have all builds from build and test stage if this is last' do
+ expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
+ end
+
+ it 'expects to have retried builds instead the original ones' do
+ retried_rspec = Ci::Build.retry(rspec_test)
+ expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
+ end
+ end
+
+ describe '#detailed_status' do
+ let(:user) { create(:user) }
+
+ it 'returns a detailed status' do
+ expect(build.detailed_status(user))
+ .to be_a Gitlab::Ci::Status::Build::Cancelable
+ end
+ end
+
+ describe 'deployment' do
+ describe '#last_deployment' do
+ subject { build.last_deployment }
+
+ context 'when multiple deployments are created' do
+ let!(:deployment1) { create(:deployment, deployable: build) }
+ let!(:deployment2) { create(:deployment, deployable: build) }
+
+ it 'returns the latest one' do
+ is_expected.to eq(deployment2)
+ end
+ end
+ end
+
+ describe '#outdated_deployment?' do
+ subject { build.outdated_deployment? }
+
+ context 'when build succeeded' do
+ let(:build) { create(:ci_build, :success) }
+ let!(:deployment) { create(:deployment, deployable: build) }
+
+ context 'current deployment is latest' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'current deployment is not latest on environment' do
+ let!(:deployment2) { create(:deployment, environment: deployment.environment) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when build failed' do
+ let(:build) { create(:ci_build, :failed) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe 'environment' do
+ describe '#has_environment?' do
+ subject { build.has_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#expanded_environment_name' do
+ subject { build.expanded_environment_name }
+
+ context 'when environment uses $CI_BUILD_REF_NAME' do
+ let(:build) do
+ create(:ci_build,
+ ref: 'master',
+ environment: 'review/$CI_BUILD_REF_NAME')
+ end
+
+ it { is_expected.to eq('review/master') }
+ end
+
+ context 'when environment uses yaml_variables containing symbol keys' do
+ let(:build) do
+ create(:ci_build,
+ yaml_variables: [{ key: :APP_HOST, value: 'host' }],
+ environment: 'review/$APP_HOST')
+ end
+
+ it { is_expected.to eq('review/host') }
+ end
+ end
+
+ describe '#starts_environment?' do
+ subject { build.starts_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and start action is defined' do
+ before do
+ build.update(options: { environment: { action: 'start' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#stops_environment?' do
+ subject { build.stops_environment? }
+
+ context 'when environment is defined' do
+ before do
+ build.update(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and stop action is defined' do
+ before do
+ build.update(options: { environment: { action: 'stop' } } )
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ build.update(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe 'erasable build' do
+ shared_examples 'erasable' do
+ it 'removes artifact file' do
+ expect(build.artifacts_file.exists?).to be_falsy
+ end
+
+ it 'removes artifact metadata file' do
+ expect(build.artifacts_metadata.exists?).to be_falsy
+ end
+
+ it 'erases build trace in trace file' do
+ expect(build.trace).to be_empty
+ end
+
+ it 'sets erased to true' do
+ expect(build.erased?).to be true
+ end
+
+ it 'sets erase date' do
+ expect(build.erased_at).not_to be_falsy
+ end
+ end
+
+ context 'build is not erasable' do
+ let!(:build) { create(:ci_build) }
+
+ describe '#erase' do
+ subject { build.erase }
+
+ it { is_expected.to be false }
+ end
+
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to eq false }
+ end
+ end
+
+ context 'build is erasable' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+
+ describe '#erase' do
+ before do
+ build.erase(erased_by: user)
+ end
+
+ context 'erased by user' do
+ let!(:user) { create(:user, username: 'eraser') }
+
+ include_examples 'erasable'
+
+ it 'records user who erased a build' do
+ expect(build.erased_by).to eq user
+ end
+ end
+
+ context 'erased by system' do
+ let(:user) { nil }
+
+ include_examples 'erasable'
+
+ it 'does not set user who erased a build' do
+ expect(build.erased_by).to be_nil
+ end
+ end
+ end
+
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#erased?' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+ subject { build.erased? }
+
+ context 'build has not been erased' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'build has been erased' do
+ before do
+ build.erase
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'metadata and build trace are not available' do
+ let!(:build) { create(:ci_build, :success, :artifacts) }
+
+ before do
+ build.remove_artifacts_metadata!
+ end
+
+ describe '#erase' do
+ it 'does not raise error' do
+ expect { build.erase }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+
+ describe '#extract_coverage' do
+ context 'valid content & regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+
+ context 'valid content & bad regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'no coverage content & regex' do
+ subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'multiple results in content & regex' do
+ subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+
+ context 'using a regex capture' do
+ subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') }
+
+ it { is_expected.to eq(65) }
+ end
+ end
+
+ describe '#first_pending' do
+ let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
+ let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
+ subject { Ci::Build.first_pending }
+
+ it { is_expected.to be_a(Ci::Build) }
+ it('returns with the first pending build') { is_expected.to eq(first) }
+ end
+
+ describe '#failed_but_allowed?' do
+ subject { build.failed_but_allowed? }
+
+ context 'when build is not allowed to fail' do
+ before do
+ build.allow_failure = false
+ end
+
+ context 'and build.status is success' do
+ before do
+ build.status = 'success'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before do
+ build.status = 'failed'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when build is allowed to fail' do
+ before do
+ build.allow_failure = true
+ end
+
+ context 'and build.status is success' do
+ before do
+ build.status = 'success'
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before do
+ build.status = 'failed'
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe 'flags' do
+ describe '#cancelable?' do
+ subject { build }
+
+ context 'when build is cancelable' do
+ context 'when build is pending' do
+ it { is_expected.to be_cancelable }
+ end
+
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.to be_cancelable }
+ end
+ end
+
+ context 'when build is not cancelable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.not_to be_cancelable }
+ end
+ end
+ end
+
+ describe '#retryable?' do
+ subject { build }
+
+ context 'when build is retryable' do
+ context 'when build is successful' do
+ before do
+ build.success!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+
+ context 'when build is failed' do
+ before do
+ build.drop!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+
+ context 'when build is canceled' do
+ before do
+ build.cancel!
+ end
+
+ it { is_expected.to be_retryable }
+ end
+ end
+
+ context 'when build is not retryable' do
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
+
+ context 'when build is skipped' do
+ before do
+ build.skip!
+ end
+
+ it { is_expected.not_to be_retryable }
+ end
+ end
+ end
+
+ describe '#manual?' do
+ before do
+ build.update(when: value)
+ end
+
+ subject { build.manual? }
+
+ context 'when is set to manual' do
+ let(:value) { 'manual' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when set to something else' do
+ let(:value) { 'something else' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#has_tags?' do
+ context 'when build has tags' do
+ subject { create(:ci_build, tag_list: ['tag']) }
+ it { is_expected.to have_tags }
+ end
+
+ context 'when build does not have tags' do
+ subject { create(:ci_build, tag_list: []) }
+ it { is_expected.not_to have_tags }
+ end
+ end
+
+ describe '#keep_artifacts!' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'to reset expire_at' do
+ build.keep_artifacts!
+
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+
+ describe '#merge_request' do
+ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
+ create(factory, source_project_id: pipeline.gl_project_id,
+ target_project_id: pipeline.gl_project_id,
+ source_branch: build.ref,
+ created_at: created_at)
+ end
+
+ context 'when a MR has a reference to the pipeline' do
+ before do
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
+
+ commits = [double(id: pipeline.sha)]
+ allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
+ end
+
+ it 'returns the single associated MR' do
+ expect(build.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+
+ context 'when there is not a MR referencing the pipeline' do
+ it 'returns nil' do
+ expect(build.merge_request).to be_nil
+ end
+ end
+
+ context 'when more than one MR have a reference to the pipeline' do
+ before do
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
+ @merge_request.close!
+ @merge_request2 = create_mr(build, pipeline, factory: :merge_request)
+
+ commits = [double(id: pipeline.sha)]
+ allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(@merge_request2).to receive(:commits).and_return(commits)
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2])
+ end
+
+ it 'returns the first MR' do
+ expect(build.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+
+ context 'when a Build is created after the MR' do
+ before do
+ @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs)
+ pipeline2 = create(:ci_pipeline, project: project)
+ @build2 = create(:ci_build, pipeline: pipeline2)
+
+ allow(@merge_request).to receive(:commits_sha).
+ and_return([pipeline.sha, pipeline2.sha])
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
+ end
+
+ it 'returns the current MR' do
+ expect(@build2.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+ end
+
+ describe '#options' do
+ let(:options) do
+ {
+ image: "ruby:2.1",
+ services: [
+ "postgres"
+ ]
+ }
+ end
+
+ it 'contains options' do
+ expect(build.options).to eq(options)
+ end
+ end
+
+ describe '#other_actions' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+ let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
+
+ subject { build.other_actions }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+
+ context 'when build is retried' do
+ let!(:new_build) { Ci::Build.retry(build) }
+
+ it 'does not return any of them' do
+ is_expected.not_to include(build, new_build)
+ end
+ end
+
+ context 'when other build is retried' do
+ let!(:retried_build) { Ci::Build.retry(other_build) }
+
+ it 'returns a retried build' do
+ is_expected.to contain_exactly(retried_build)
+ end
+ end
+ end
+
+ describe '#persisted_environment' do
+ before do
+ @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+ end
+
+ subject { build.persisted_environment }
+
+ context 'referenced literally' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+ it { is_expected.to eq(@environment) }
+ end
+
+ context 'referenced with a variable' do
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+
+ it { is_expected.to eq(@environment) }
+ end
+ end
+
+ describe '#play' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ subject { build.play }
+
+ it 'enqueues a build' do
+ is_expected.to be_pending
+ is_expected.to eq(build)
+ end
+
+ context 'for successful build' do
+ before do
+ build.update(status: 'success')
+ end
+
+ it 'creates a new build' do
+ is_expected.to be_pending
+ is_expected.not_to eq(build)
+ end
+ end
+ end
+
+ describe 'project settings' do
+ describe '#timeout' do
+ it 'returns project timeout configuration' do
+ expect(build.timeout).to eq(project.build_timeout)
+ end
+ end
+
+ describe '#allow_git_fetch' do
+ it 'return project allow_git_fetch configuration' do
+ expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch)
+ end
+ end
+ end
+
+ describe '#project' do
+ subject { build.project }
+
+ it { is_expected.to eq(pipeline.project) }
+ end
+
+ describe '#project_id' do
+ subject { build.project_id }
+
+ it { is_expected.to eq(pipeline.project_id) }
+ end
+
+ describe '#project_name' do
+ subject { build.project_name }
+
+ it { is_expected.to eq(project.name) }
+ end
+
+ describe '#raw_trace' do
+ subject { build.raw_trace }
+
+ context 'when build.trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update(runners_token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(token: token)
+ build.update(trace: token)
+ end
+
+ it { is_expected.not_to include(token) }
+ end
+ end
+
+ describe '#ref_slug' do
+ {
+ 'master' => 'master',
+ '1-foo' => '1-foo',
+ 'fix/1-foo' => 'fix-1-foo',
+ 'fix-1-foo' => 'fix-1-foo',
+ 'a' * 63 => 'a' * 63,
+ 'a' * 64 => 'a' * 63,
+ 'FOO' => 'foo',
+ }.each do |ref, slug|
+ it "transforms #{ref} to #{slug}" do
+ build.ref = ref
+
+ expect(build.ref_slug).to eq(slug)
+ end
+ end
+ end
+
+ describe '#repo_url' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+
+ subject { build.repo_url }
+
+ it { is_expected.to be_a(String) }
+ it { is_expected.to end_with(".git") }
+ it { is_expected.to start_with(project.web_url[0..6]) }
+ it { is_expected.to include(build.token) }
+ it { is_expected.to include('gitlab-ci-token') }
+ it { is_expected.to include(project.web_url[7..-1]) }
+ end
+
+ describe '#stuck?' do
+ subject { build.stuck? }
+
+ context "when commit_status.status is pending" do
+ before do
+ build.status = 'pending'
+ end
+
+ it { is_expected.to be_truthy }
+
+ context "and there are specific runner" do
+ let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
+
+ before do
+ build.project.runners << runner
+ runner.save
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ %w[success failed canceled running].each do |state|
+ context "when commit_status.status is #{state}" do
+ before do
+ build.status = state
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
describe '#trace' do
it 'obfuscates project runners token' do
@@ -24,6 +972,63 @@ describe Ci::Build, models: true do
expect(build.trace).to eq(test_trace)
end
+
+ context 'when build does not have trace' do
+ it 'is is empty' do
+ expect(build.trace).to be_nil
+ end
+ end
+
+ context 'when trace contains text' do
+ let(:text) { 'example output' }
+ before do
+ build.trace = text
+ end
+
+ it { expect(build.trace).to eq(text) }
+ end
+
+ context 'when trace hides runners token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.project.update(runners_token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+
+ context 'when build.trace hides build token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.update(trace: token)
+ build.update(token: token)
+ end
+
+ it { expect(build.trace).not_to include(token) }
+ it { expect(build.raw_trace).to include(token) }
+ end
+ end
+
+ describe '#has_expiring_artifacts?' do
+ context 'when artifacts have expiration date set' do
+ before { build.update(artifacts_expire_at: 1.day.from_now) }
+
+ it 'has expiring artifacts' do
+ expect(build).to have_expiring_artifacts
+ end
+ end
+
+ context 'when artifacts do not have expiration date set' do
+ before { build.update(artifacts_expire_at: nil) }
+
+ it 'does not have expiring artifacts' do
+ expect(build).not_to have_expiring_artifacts
+ end
+ end
end
describe '#has_trace_file?' do
@@ -111,4 +1116,299 @@ describe Ci::Build, models: true do
build.destroy
end
end
+
+ describe '#when' do
+ subject { build.when }
+
+ context 'when `when` is undefined' do
+ before do
+ build.when = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config has `when`' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ when: 'always'
+ }
+ })
+ end
+
+ it { is_expected.to eq('always') }
+ end
+ end
+ end
+ end
+
+ describe '#variables' do
+ let(:container_registry_enabled) { false }
+ let(:predefined_variables) do
+ [
+ { key: 'CI', value: 'true', public: true },
+ { key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
+ { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
+ { key: 'CI_BUILD_REF', value: build.sha, public: true },
+ { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
+ { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
+ { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
+ { key: 'CI_BUILD_NAME', value: 'test', public: true },
+ { key: 'CI_BUILD_STAGE', value: 'test', public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
+ { key: 'CI_PROJECT_NAME', value: project.path, public: true },
+ { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
+ { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
+ ]
+ end
+
+ before do
+ stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com')
+ end
+
+ subject { build.variables }
+
+ context 'returns variables' do
+ before do
+ build.yaml_variables = []
+ end
+
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when build has user' do
+ let(:user) { create(:user, username: 'starter') }
+ let(:user_variables) do
+ [
+ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
+ ]
+ end
+
+ before do
+ build.update_attributes(user: user)
+ end
+
+ it { user_variables.each { |v| is_expected.to include(v) } }
+ end
+
+ context 'when build has an environment' do
+ before do
+ build.update(environment: 'production')
+ create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
+ end
+
+ let(:environment_variables) do
+ [
+ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
+ ]
+ end
+
+ it { environment_variables.each { |v| is_expected.to include(v) } }
+ end
+
+ context 'when build started manually' do
+ before do
+ build.update_attributes(when: :manual)
+ end
+
+ let(:manual_variable) do
+ { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
+ end
+
+ it { is_expected.to include(manual_variable) }
+ end
+
+ context 'when build is for tag' do
+ let(:tag_variable) do
+ { key: 'CI_BUILD_TAG', value: 'master', public: true }
+ end
+
+ before do
+ build.update_attributes(tag: true)
+ end
+
+ it { is_expected.to include(tag_variable) }
+ end
+
+ context 'when secure variable is defined' do
+ let(:secure_variable) do
+ { key: 'SECRET_KEY', value: 'secret_value', public: false }
+ end
+
+ before do
+ build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ end
+
+ it { is_expected.to include(secure_variable) }
+ end
+
+ context 'when build is for triggers' do
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
+ let(:user_trigger_variable) do
+ { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
+ end
+ let(:predefined_trigger_variable) do
+ { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
+ end
+
+ before do
+ build.trigger_request = trigger_request
+ end
+
+ it { is_expected.to include(user_trigger_variable) }
+ it { is_expected.to include(predefined_trigger_variable) }
+ end
+
+ context 'when yaml_variables are undefined' do
+ before do
+ build.yaml_variables = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when config has variables' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ variables: {
+ KEY: 'value'
+ }
+ }
+ })
+ end
+ let(:variables) do
+ [{ key: 'KEY', value: 'value', public: true }]
+ end
+
+ it { is_expected.to eq(predefined_variables + variables) }
+ end
+ end
+ end
+
+ context 'when container registry is enabled' do
+ let(:container_registry_enabled) { true }
+ let(:ci_registry) do
+ { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
+ end
+ let(:ci_registry_image) do
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
+ end
+
+ context 'and is disabled for project' do
+ before do
+ project.update(container_registry_enabled: false)
+ end
+
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.not_to include(ci_registry_image) }
+ end
+
+ context 'and is enabled for project' do
+ before do
+ project.update(container_registry_enabled: true)
+ end
+
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.to include(ci_registry_image) }
+ end
+ end
+
+ context 'when runner is assigned to build' do
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
+
+ before do
+ build.update(runner: runner)
+ end
+
+ it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
+ end
+
+ context 'when build is for a deployment' do
+ let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
+
+ before do
+ build.environment = 'production'
+ allow(project).to receive(:deployment_variables).and_return([deployment_variable])
+ end
+
+ it { is_expected.to include(deployment_variable) }
+ end
+
+ context 'returns variables in valid order' do
+ before do
+ allow(build).to receive(:predefined_variables) { ['predefined'] }
+ allow(project).to receive(:predefined_variables) { ['project'] }
+ allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
+ allow(build).to receive(:yaml_variables) { ['yaml'] }
+ allow(project).to receive(:secret_variables) { ['secret'] }
+ end
+
+ 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/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/commit_spec.rb b/spec/models/commit_spec.rb
index 74b50d2908d..0d425ab7fd4 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -323,4 +323,32 @@ 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
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 701f3323c0f..64ea607eb95 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -243,4 +243,23 @@ describe CommitStatus, models: true do
.to be_a Gitlab::Ci::Status::Success
end
end
+
+ describe '#sortable_name' do
+ tests = {
+ 'karma' => ['karma'],
+ 'karma 0 20' => ['karma ', 0, ' ', 20],
+ 'karma 10 20' => ['karma ', 10, ' ', 20],
+ 'karma 50:100' => ['karma ', 50, ':', 100],
+ 'karma 1.10' => ['karma ', 1, '.', 10],
+ 'karma 1.5.1' => ['karma ', 1, '.', 5, '.', 1],
+ 'karma 1 a' => ['karma ', 1, ' a']
+ }
+
+ tests.each do |name, sortable_name|
+ it "'#{name}' sorts as '#{sortable_name}'" do
+ commit_status.name = name
+ expect(commit_status.sortable_name).to eq(sortable_name)
+ end
+ 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/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_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index eb876d105da..6d599e148a2 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -76,6 +76,32 @@ describe MergeRequestDiff, models: true do
end
end
+ describe '#save_diffs' do
+ it 'saves collected state' do
+ mr_diff = create(:merge_request).merge_request_diff
+
+ expect(mr_diff.collected?).to be_truthy
+ end
+
+ it 'saves overflow state' do
+ allow(Commit).to receive(:max_diff_options)
+ .and_return(max_lines: 0, max_files: 0)
+
+ mr_diff = create(:merge_request).merge_request_diff
+
+ expect(mr_diff.overflow?).to be_truthy
+ end
+
+ it 'saves empty state' do
+ allow_any_instance_of(MergeRequestDiff).to receive(:commits)
+ .and_return([])
+
+ mr_diff = create(:merge_request).merge_request_diff
+
+ expect(mr_diff.empty?).to be_truthy
+ end
+ end
+
describe '#commits_sha' do
it 'returns all commits SHA using serialized commits' do
subject.st_commits = [
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8d1385016fd..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,
@@ -1514,6 +1522,108 @@ describe MergeRequest, models: true do
end
end
+ describe '#mergeable_with_slash_command?' do
+ def create_pipeline(status)
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ status: status)
+ end
+
+ 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) }
+ let(:mr_sha) { merge_request.diff_head_sha }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ context 'when autocomplete_precheck is set to true' do
+ it 'is mergeable by developer' do
+ expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy
+ end
+
+ it 'is not mergeable by normal user' do
+ expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey
+ end
+ end
+
+ context 'when autocomplete_precheck is set to false' do
+ it 'is mergeable by developer' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+
+ it 'is not mergeable by normal user' do
+ expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey
+ end
+
+ context 'closed MR' do
+ before do
+ merge_request.update_attribute(:state, :closed)
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'MR with WIP' do
+ before do
+ merge_request.update_attribute(:title, 'WIP: some MR')
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'sha differs from the MR diff_head_sha' do
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey
+ end
+ end
+
+ context 'sha is not provided' do
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer)).to be_falsey
+ end
+ end
+
+ context 'with pipeline ok' do
+ before do
+ create_pipeline(:success)
+ end
+
+ it 'is mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+ end
+
+ context 'with failing pipeline' do
+ before do
+ create_pipeline(:failed)
+ end
+
+ it 'is not mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ end
+ end
+
+ context 'with running pipeline' do
+ before do
+ create_pipeline(:running)
+ end
+
+ it 'is mergeable' do
+ expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ end
+ end
+ end
+ end
+
describe '#has_commits?' do
before do
allow(subject.merge_request_diff).to receive(:commits_count).
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index d7e1a4e3b6c..497a626a418 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -1,14 +1,28 @@
require 'spec_helper'
-describe BambooService, models: true do
+describe BambooService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
+ let(:bamboo_url) { 'http://gitlab.com/bamboo' }
+
+ subject(:service) do
+ described_class.create(
+ project: create(:empty_project),
+ properties: {
+ bamboo_url: bamboo_url,
+ username: 'mic',
+ password: 'password',
+ build_key: 'foo'
+ }
+ )
+ end
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'Validations' do
- subject { service }
-
context 'when service is active' do
before { subject.active = true }
@@ -103,90 +117,103 @@ describe BambooService, models: true do
end
describe '#build_page' do
- it 'returns a specific URL when status is 500' do
- stub_request(status: 500)
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref')
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
+ expect(service.build_page('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a specific URL when response has no results' do
- stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+ describe '#commit_status' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref')
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
+ expect(service.commit_status('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a build URL when bamboo_url has no trailing slash' do
- stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
+ describe '#calculate_reactive_cache' do
+ context '#build_page' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:build_page] }
- expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
- end
+ it 'returns a specific URL when status is 500' do
+ stub_request(status: 500)
- it 'returns a build URL when bamboo_url has a trailing slash' do
- stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
+ is_expected.to eq('http://gitlab.com/bamboo/browse/foo')
+ end
- expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
- end
- end
+ it 'returns a specific URL when response has no results' do
+ stub_request(body: bamboo_response(size: 0))
- describe '#commit_status' do
- it 'sets commit status to :error when status is 500' do
- stub_request(status: 500)
+ is_expected.to eq('http://gitlab.com/bamboo/browse/foo')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
+ it 'returns a build URL when bamboo_url has no trailing slash' do
+ stub_request(body: bamboo_response)
- it 'sets commit status to "pending" when status is 404' do
- stub_request(status: 404)
+ is_expected.to eq('http://gitlab.com/bamboo/browse/42')
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ context 'bamboo_url has trailing slash' do
+ let(:bamboo_url) { 'http://gitlab.com/bamboo/' }
- it 'sets commit status to "pending" when response has no results' do
- stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
+ it 'returns a build URL' do
+ stub_request(body: bamboo_response)
- expect(service.commit_status('123', 'unused')).to eq('pending')
+ is_expected.to eq('http://gitlab.com/bamboo/browse/42')
+ end
+ end
end
- it 'sets commit status to "success" when build state contains Success' do
- stub_request(build_state: 'YAY Success!')
+ context '#commit_status' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
- expect(service.commit_status('123', 'unused')).to eq('success')
- end
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
- it 'sets commit status to "failed" when build state contains Failed' do
- stub_request(build_state: 'NO Failed!')
+ is_expected.to eq(:error)
+ end
- expect(service.commit_status('123', 'unused')).to eq('failed')
- end
+ it 'sets commit status to "pending" when status is 404' do
+ stub_request(status: 404)
- it 'sets commit status to "pending" when build state contains Pending' do
- stub_request(build_state: 'NO Pending!')
+ is_expected.to eq('pending')
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ it 'sets commit status to "pending" when response has no results' do
+ stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
- it 'sets commit status to :error when build state is unknown' do
- stub_request(build_state: 'FOO BAR!')
+ is_expected.to eq('pending')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
- end
+ it 'sets commit status to "success" when build state contains Success' do
+ stub_request(body: bamboo_response(build_state: 'YAY Success!'))
- def service(bamboo_url: 'http://gitlab.com/bamboo')
- described_class.create(
- project: create(:empty_project),
- properties: {
- bamboo_url: bamboo_url,
- username: 'mic',
- password: 'password',
- build_key: 'foo'
- }
- )
+ is_expected.to eq('success')
+ end
+
+ it 'sets commit status to "failed" when build state contains Failed' do
+ stub_request(body: bamboo_response(build_state: 'NO Failed!'))
+
+ is_expected.to eq('failed')
+ end
+
+ it 'sets commit status to "pending" when build state contains Pending' do
+ stub_request(body: bamboo_response(build_state: 'NO Pending!'))
+
+ is_expected.to eq('pending')
+ end
+
+ it 'sets commit status to :error when build state is unknown' do
+ stub_request(body: bamboo_response(build_state: 'FOO BAR!'))
+
+ is_expected.to eq(:error)
+ end
+ end
end
- def stub_request(status: 200, body: nil, build_state: 'success')
+ def stub_request(status: 200, body: nil)
bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
- body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}})
WebMock.stub_request(:get, bamboo_full_url).to_return(
status: status,
@@ -194,4 +221,8 @@ describe BambooService, models: true do
body: body
)
end
+
+ def bamboo_response(result_key: 42, build_state: 'success', size: 1)
+ %Q({"results":{"results":{"size":"#{size}","result":{"buildState":"#{build_state}","planResultKey":{"key":"#{result_key}"}}}}})
+ end
end
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 6f65beb79d0..dbd23ff5491 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -1,6 +1,21 @@
require 'spec_helper'
-describe BuildkiteService, models: true do
+describe BuildkiteService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
+ let(:project) { create(:empty_project) }
+
+ subject(:service) do
+ described_class.create(
+ project: project,
+ properties: {
+ service_hook: true,
+ project_url: 'https://buildkite.com/account-name/example-project',
+ token: 'secret-sauce-webhook-token:secret-sauce-status-token'
+ }
+ )
+ end
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -25,21 +40,12 @@ describe BuildkiteService, models: true do
describe 'commits methods' do
before do
- @project = Project.new
- allow(@project).to receive(:default_branch).and_return('default-brancho')
-
- @service = BuildkiteService.new
- allow(@service).to receive_messages(
- project: @project,
- service_hook: true,
- project_url: 'https://buildkite.com/account-name/example-project',
- token: 'secret-sauce-webhook-token:secret-sauce-status-token'
- )
+ allow(project).to receive(:default_branch).and_return('default-brancho')
end
describe '#webhook_url' do
it 'returns the webhook url' do
- expect(@service.webhook_url).to eq(
+ expect(service.webhook_url).to eq(
'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token'
)
end
@@ -47,7 +53,7 @@ describe BuildkiteService, models: true do
describe '#commit_status_path' do
it 'returns the correct status page' do
- expect(@service.commit_status_path('2ab7834c')).to eq(
+ expect(service.commit_status_path('2ab7834c')).to eq(
'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c'
)
end
@@ -55,10 +61,53 @@ describe BuildkiteService, models: true do
describe '#build_page' do
it 'returns the correct build page' do
- expect(@service.build_page('2ab7834c', nil)).to eq(
+ expect(service.build_page('2ab7834c', nil)).to eq(
'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c'
)
end
end
+
+ describe '#commit_status' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref')
+
+ expect(service.commit_status('sha', 'ref')).to eq('foo')
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ context '#commit_status' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
+
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
+
+ is_expected.to eq(:error)
+ end
+
+ it 'sets commit status to :error when status is 404' do
+ stub_request(status: 404)
+
+ is_expected.to eq(:error)
+ end
+
+ it 'passes through build status untouched when status is 200' do
+ stub_request(body: %Q({"status":"Great Success"}))
+
+ is_expected.to eq('Great Success')
+ end
+ end
+ end
+ end
+
+ def stub_request(status: 200, body: nil)
+ body ||= %Q({"status":"success"})
+ buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
+
+ WebMock.stub_request(:get, buildkite_full_url).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
end
end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index f13bb1e8adf..42c2ed668bc 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
-describe DroneCiService, models: true do
+describe DroneCiService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:service_hook) }
@@ -33,6 +35,10 @@ describe DroneCiService, models: true do
let(:token) { 'secret' }
let(:iid) { rand(1..9999) }
+ # URL's
+ let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
+ let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
+
before(:each) do
allow(drone).to receive_messages(
project_id: project.id,
@@ -42,22 +48,66 @@ describe DroneCiService, models: true do
token: token
)
end
+
+ def stub_request(status: 200, body: nil)
+ body ||= %Q({"status":"success"})
+
+ WebMock.stub_request(:get, commit_status_path).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
+ end
end
describe "service page/path methods" do
include_context :drone_ci_service
- # URL's
- let(:commit_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
- let(:merge_request_page) { "#{drone_url}/gitlab/#{path}/redirect/pulls/#{iid}" }
- let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
- let(:merge_request_status_path) { "#{drone_url}/gitlab/#{path}/pulls/#{iid}?access_token=#{token}" }
-
- it { expect(drone.build_page(sha, branch)).to eq(commit_page) }
- it { expect(drone.commit_page(sha, branch)).to eq(commit_page) }
- it { expect(drone.merge_request_page(iid, sha, branch)).to eq(merge_request_page) }
+ it { expect(drone.build_page(sha, branch)).to eq(build_page) }
it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) }
- it { expect(drone.merge_request_status_path(iid, sha, branch)).to eq(merge_request_status_path) }
+ end
+
+ describe '#commit_status' do
+ include_context :drone_ci_service
+
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(drone, { commit_status: 'foo' }, 'sha', 'ref')
+
+ expect(drone.commit_status('sha', 'ref')).to eq('foo')
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ include_context :drone_ci_service
+
+ context '#commit_status' do
+ subject { drone.calculate_reactive_cache(sha, branch)[:commit_status] }
+
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
+
+ is_expected.to eq(:error)
+ end
+
+ it 'sets commit status to :error when status is 404' do
+ stub_request(status: 404)
+
+ is_expected.to eq(:error)
+ end
+
+ { "killed" => :canceled,
+ "failure" => :failed,
+ "error" => :failed,
+ "success" => "success",
+ }.each do |drone_status, our_status|
+
+ it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do
+ stub_request(body: %Q({"status":"#{drone_status}"}))
+
+ is_expected.to eq(our_status)
+ end
+ end
+ end
end
describe "execute" do
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index f7e878844dc..a1edd083aa1 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -1,14 +1,28 @@
require 'spec_helper'
-describe TeamcityService, models: true do
+describe TeamcityService, models: true, caching: true do
+ include ReactiveCachingHelpers
+
+ let(:teamcity_url) { 'http://gitlab.com/teamcity' }
+
+ subject(:service) do
+ described_class.create(
+ project: create(:empty_project),
+ properties: {
+ teamcity_url: teamcity_url,
+ username: 'mic',
+ password: 'password',
+ build_type: 'foo'
+ }
+ )
+ end
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'Validations' do
- subject { service }
-
context 'when service is active' do
before { subject.active = true }
@@ -103,73 +117,87 @@ describe TeamcityService, models: true do
end
describe '#build_page' do
- it 'returns a specific URL when status is 500' do
- stub_request(status: 500)
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref')
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
+ expect(service.build_page('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a build URL when teamcity_url has no trailing slash' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ describe '#commit_status' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref')
- expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service.commit_status('sha', 'ref')).to eq('foo')
end
+ end
- it 'returns a build URL when teamcity_url has a trailing slash' do
- stub_request(body: %Q({"build":{"id":"666"}}))
+ describe '#calculate_reactive_cache' do
+ context 'build_page' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:build_page] }
- expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
- end
- end
+ it 'returns a specific URL when status is 500' do
+ stub_request(status: 500)
- describe '#commit_status' do
- it 'sets commit status to :error when status is 500' do
- stub_request(status: 500)
+ is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
+ it 'returns a build URL when teamcity_url has no trailing slash' do
+ stub_request(body: %Q({"build":{"id":"666"}}))
- it 'sets commit status to "pending" when status is 404' do
- stub_request(status: 404)
+ is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ context 'teamcity_url has trailing slash' do
+ let(:teamcity_url) { 'http://gitlab.com/teamcity/' }
- it 'sets commit status to "success" when build status contains SUCCESS' do
- stub_request(build_status: 'YAY SUCCESS!')
+ it 'returns a build URL' do
+ stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service.commit_status('123', 'unused')).to eq('success')
+ is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
+ end
+ end
end
- it 'sets commit status to "failed" when build status contains FAILURE' do
- stub_request(build_status: 'NO FAILURE!')
+ context 'commit_status' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
- expect(service.commit_status('123', 'unused')).to eq('failed')
- end
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
- it 'sets commit status to "pending" when build status contains Pending' do
- stub_request(build_status: 'NO Pending!')
+ is_expected.to eq(:error)
+ end
- expect(service.commit_status('123', 'unused')).to eq('pending')
- end
+ it 'sets commit status to "pending" when status is 404' do
+ stub_request(status: 404)
- it 'sets commit status to :error when build status is unknown' do
- stub_request(build_status: 'FOO BAR!')
+ is_expected.to eq('pending')
+ end
- expect(service.commit_status('123', 'unused')).to eq(:error)
- end
- end
+ it 'sets commit status to "success" when build status contains SUCCESS' do
+ stub_request(build_status: 'YAY SUCCESS!')
- def service(teamcity_url: 'http://gitlab.com/teamcity')
- described_class.create(
- project: create(:empty_project),
- properties: {
- teamcity_url: teamcity_url,
- username: 'mic',
- password: 'password',
- build_type: 'foo'
- }
- )
+ is_expected.to eq('success')
+ end
+
+ it 'sets commit status to "failed" when build status contains FAILURE' do
+ stub_request(build_status: 'NO FAILURE!')
+
+ is_expected.to eq('failed')
+ end
+
+ it 'sets commit status to "pending" when build status contains Pending' do
+ stub_request(build_status: 'NO Pending!')
+
+ is_expected.to eq('pending')
+ end
+
+ it 'sets commit status to :error when build status is unknown' do
+ stub_request(build_status: 'FOO BAR!')
+
+ is_expected.to eq(:error)
+ end
+ end
end
def stub_request(status: 200, body: nil, build_status: 'success')
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 3ec7bb46686..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' }
@@ -190,34 +188,54 @@ describe Project, models: true do
end
it 'does not allow an invalid URI as import_url' do
- project2 = build(:project, import_url: 'invalid://')
+ project2 = build(:empty_project, import_url: 'invalid://')
expect(project2).not_to be_valid
end
it 'does allow a valid URI as import_url' do
- project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
+ project2 = build(:empty_project, import_url: 'ssh://test@gitlab.com/project.git')
expect(project2).to be_valid
end
it 'allows an empty URI' do
- project2 = build(:project, import_url: '')
+ project2 = build(:empty_project, import_url: '')
expect(project2).to be_valid
end
it 'does not produce import data on an empty URI' do
- project2 = build(:project, import_url: '')
+ project2 = build(:empty_project, import_url: '')
expect(project2.import_data).to be_nil
end
it 'does not produce import data on an invalid URI' do
- project2 = build(:project, import_url: 'test://')
+ project2 = build(:empty_project, import_url: 'test://')
expect(project2.import_data).to be_nil
end
+
+ describe 'project pending deletion' do
+ let!(:project_pending_deletion) do
+ create(:empty_project,
+ pending_delete: true)
+ end
+ let(:new_project) do
+ build(:empty_project,
+ name: project_pending_deletion.name,
+ namespace: project_pending_deletion.namespace)
+ end
+
+ before do
+ new_project.validate
+ end
+
+ it 'contains errors related to the project being deleted' do
+ expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.')
+ end
+ end
end
describe 'default_scope' do
@@ -332,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')
@@ -392,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
@@ -476,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') }
@@ -502,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)
@@ -510,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
@@ -558,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])
@@ -580,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
@@ -592,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
@@ -643,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')
@@ -665,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)
@@ -687,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)
@@ -720,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')
@@ -736,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
@@ -771,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
@@ -805,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 }
@@ -857,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) }
@@ -955,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 }
@@ -964,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 }
@@ -974,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])
@@ -1032,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
@@ -1082,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) }
@@ -1103,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])
@@ -1122,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 }
@@ -1135,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
@@ -1177,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")
@@ -1361,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
@@ -1441,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)
@@ -1452,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
@@ -1525,11 +1545,13 @@ describe Project, models: true do
end
end
- describe 'change_head' do
- let(:project) { create(:project) }
+ describe '#change_head' do
+ let(:project) { create(:project, :repository) }
- it 'calls the before_change_head method' do
+ it 'calls the before_change_head and after_change_head methods' do
expect(project.repository).to receive(:before_change_head)
+ expect(project.repository).to receive(:after_change_head)
+
project.change_head(project.default_branch)
end
@@ -1545,11 +1567,6 @@ describe Project, models: true do
project.change_head(project.default_branch)
end
- it 'expires the avatar cache' do
- expect(project.repository).to receive(:expire_avatar_cache)
- project.change_head(project.default_branch)
- end
-
it 'reloads the default branch' do
expect(project).to receive(:reload_default_branch)
project.change_head(project.default_branch)
@@ -1557,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
@@ -1579,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
@@ -1593,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
@@ -1609,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
@@ -1661,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/repository_spec.rb b/spec/models/repository_spec.rb
index af7e89eae05..99ca53938c8 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1150,6 +1150,24 @@ describe Repository, models: true do
end
end
+ describe '#after_change_head' do
+ it 'flushes the readme cache' do
+ expect(repository).to receive(:expire_method_caches).with([
+ :readme,
+ :changelog,
+ :license,
+ :contributing,
+ :version,
+ :gitignore,
+ :koding,
+ :gitlab_ci,
+ :avatar
+ ])
+
+ repository.after_change_head
+ end
+ end
+
describe '#before_push_tag' do
it 'flushes the cache' do
expect(repository).to receive(:expire_statistics_caches)
@@ -1513,14 +1531,6 @@ describe Repository, models: true do
end
end
- describe '#expire_avatar_cache' do
- it 'expires the cache' do
- expect(repository).to receive(:expire_method_caches).with(%i(avatar))
-
- repository.expire_avatar_cache
- end
- end
-
describe '#file_on_head' do
context 'with a non-existing repository' do
it 'returns nil' do
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..ca3d4ff0aa9 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) }
@@ -927,8 +929,8 @@ describe User, models: true 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
@@ -954,7 +956,7 @@ describe User, models: true 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)
@@ -994,9 +996,9 @@ describe User, models: true do
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 +1040,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 +1115,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 +1123,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 +1132,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 +1142,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 +1154,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 +1166,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 +1177,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 +1192,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 +1203,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 +1212,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 +1235,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 +1244,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 +1275,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 +1285,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 +1315,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..5c14db067a8 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
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..a3798c8cd6c 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]
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index a786dc9edb3..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,
@@ -50,6 +50,8 @@ describe API::Issues, api: true do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+ let(:no_milestone_title) { URI.escape(Milestone::None.title) }
+
before do
project.team << [user, :reporter]
project.team << [guest, :guest]
@@ -107,6 +109,7 @@ describe API::Issues, api: true do
it 'returns an array of labeled issues when at least one label matches' do
get api("/issues?labels=#{label.title},foo,bar", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -136,6 +139,51 @@ describe API::Issues, api: true do
expect(json_response.length).to eq(0)
end
+ it 'returns an empty array if no issue matches milestone' do
+ get api("/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api("/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api("/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api("/issues?milestone=#{milestone.title}"\
+ '&state=closed', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api("/issues?milestone=#{no_milestone_title}", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
it 'sorts by created_at descending by default' do
get api('/issues', user)
response_dates = json_response.map { |issue| issue['created_at'] }
@@ -176,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,
@@ -318,6 +366,15 @@ describe API::Issues, api: true do
expect(json_response.first['id']).to eq(group_closed_issue.id)
end
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_confidential_issue.id)
+ end
+
it 'sorts by created_at descending by default' do
get api(base_url, user)
response_dates = json_response.map { |issue| issue['created_at'] }
@@ -357,7 +414,6 @@ describe API::Issues, api: true do
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
- let(:title) { milestone.title }
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
@@ -433,8 +489,9 @@ describe API::Issues, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'returns an array of labeled project issues when at least one label matches' do
+ it 'returns an array of labeled project issues where all labels match' do
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -463,7 +520,8 @@ describe API::Issues, api: true do
end
it 'returns an array of issues in given milestone' do
- get api("#{base_url}/issues?milestone=#{title}", user)
+ get api("#{base_url}/issues?milestone=#{milestone.title}", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
@@ -480,6 +538,15 @@ describe API::Issues, api: true do
expect(json_response.first['id']).to eq(closed_issue.id)
end
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
it 'sorts by created_at descending by default' do
get api("#{base_url}/issues", user)
response_dates = json_response.map { |issue| issue['created_at'] }
@@ -547,12 +614,21 @@ describe API::Issues, api: true do
it 'returns a project issue by iid' do
get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
+
expect(response.status).to eq 200
+ expect(json_response.length).to eq 1
expect(json_response.first['title']).to eq issue.title
expect(json_response.first['id']).to eq issue.id
expect(json_response.first['iid']).to eq issue.iid
end
+ it 'returns an empty array for an unknown project issue iid' do
+ get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.length).to eq 0
+ end
+
it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404)
@@ -976,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)
@@ -995,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),
@@ -1117,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..6f20ac49269 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]
@@ -671,6 +671,12 @@ describe API::MergeRequests, api: true do
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
merge_request
merge_request.created_at += 1.hour
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 028f93c8561..0f8d054b31e 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
@@ -265,7 +265,7 @@ describe API::Notes, api: true do
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 f5788d15f93..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
@@ -1085,7 +1085,7 @@ describe API::Projects, api: true do
end
describe 'GET /projects/search/:query' do
- let!(:query) { 'query'}
+ let!(:query) { 'query'}
let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
@@ -1095,32 +1095,36 @@ describe API::Projects, api: true do
let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
let!(:public) { create(:empty_project, :public, name: "public #{query}") }
let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
+ let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
shared_examples_for 'project search response' do |args = {}|
it 'returns project search responses' do
- get api("/projects/search/#{query}", current_user)
+ get api("/projects/search/#{args[:query]}", current_user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(args[:results])
- json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) }
+ json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
end
end
context 'when unauthenticated' do
- it_behaves_like 'project search response', results: 1 do
+ it_behaves_like 'project search response', query: 'query', results: 1 do
let(:current_user) { nil }
end
end
context 'when authenticated' do
- it_behaves_like 'project search response', results: 6 do
+ it_behaves_like 'project search response', query: 'query', results: 6 do
+ let(:current_user) { user }
+ end
+ 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
- it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do
+ it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
let(:current_user) { user2 }
end
end
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/settings_spec.rb b/spec/requests/api/settings_spec.rb
index ad9d8a25af4..91e3c333a02 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -16,6 +16,8 @@ describe API::Settings, 'Settings', api: true do
expect(json_response['repository_storage']).to eq('default')
expect(json_response['koding_enabled']).to be_falsey
expect(json_response['koding_url']).to be_nil
+ expect(json_response['plantuml_enabled']).to be_falsey
+ expect(json_response['plantuml_url']).to be_nil
end
end
@@ -28,7 +30,8 @@ describe API::Settings, 'Settings', api: true do
it "updates application settings" do
put api("/application/settings", admin),
- default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com'
+ default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com',
+ plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com'
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['signin_enabled']).to be_falsey
@@ -36,6 +39,8 @@ describe API::Settings, 'Settings', api: true do
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['koding_enabled']).to be_truthy
expect(json_response['koding_url']).to eq('http://koding.example.com')
+ expect(json_response['plantuml_enabled']).to be_truthy
+ expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
end
end
@@ -47,5 +52,14 @@ describe API::Settings, 'Settings', api: true do
expect(json_response['error']).to eq('koding_url is missing')
end
end
+
+ context "missing plantuml_url value when plantuml_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), plantuml_enabled: true
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('plantuml_url is missing')
+ end
+ end
end
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 887a2ba5b84..6fe695626ca 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') }
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/users_spec.rb b/spec/requests/api/users_spec.rb
index 2c2e17eddb0..5bf5bf0739e 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -137,6 +137,15 @@ describe API::Users, api: true do
expect(new_user.can_create_group).to eq(true)
end
+ it "creates user with optional attributes" do
+ optional_attributes = { confirm: true }
+ attributes = attributes_for(:user).merge(optional_attributes)
+
+ post api('/users', admin), attributes
+
+ expect(response).to have_http_status(201)
+ end
+
it "creates non-admin user" do
post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
expect(response).to have_http_status(201)
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/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
index 383704572b1..0f7be8b2c39 100644
--- a/spec/serializers/build_action_entity_spec.rb
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -10,8 +10,8 @@ describe BuildActionEntity do
describe '#as_json' do
subject { entity.as_json }
- it 'contains humanized build name' do
- expect(subject[:name]).to eq 'Test build'
+ it 'contains original build name' do
+ expect(subject[:name]).to eq 'test_build'
end
it 'contains path to the action play' do
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
index a8662e81d20..0333d73b5b5 100644
--- a/spec/serializers/commit_entity_spec.rb
+++ b/spec/serializers/commit_entity_spec.rb
@@ -33,10 +33,12 @@ describe CommitEntity do
it 'contains path to commit' do
expect(subject).to include(:commit_path)
+ expect(subject[:commit_path]).to include "commit/#{commit.id}"
end
it 'contains URL to commit' do
expect(subject).to include(:commit_url)
+ expect(subject[:commit_path]).to include "commit/#{commit.id}"
end
it 'needs to receive project in the request' do
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..00d0e20f47c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -237,6 +237,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/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 88c786947d3..7d73c0ea5d0 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -121,6 +121,99 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'merge' do
+ let(:opts) do
+ {
+ merge: merge_request.diff_head_sha
+ }
+ end
+
+ let(:service) { MergeRequests::UpdateService.new(project, user, opts) }
+
+ context 'without pipeline' do
+ before do
+ merge_request.merge_error = 'Error'
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request).to be_valid }
+ it { expect(@merge_request.state).to eq('merged') }
+ it { expect(@merge_request.merge_error).to be_nil }
+ end
+
+ context 'with finished pipeline' do
+ before do
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ status: :success)
+
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request).to be_valid }
+ it { expect(@merge_request.state).to eq('merged') }
+ end
+
+ context 'with active pipeline' do
+ before do
+ service_mock = double
+ create(:ci_pipeline_with_one_job,
+ project: project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+
+ expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user).
+ and_return(service_mock)
+ expect(service_mock).to receive(:execute).with(merge_request)
+ end
+
+ it { service.execute(merge_request) }
+ end
+
+ context 'with a non-authorised user' do
+ let(:visitor) { create(:user) }
+ let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) }
+
+ before do
+ merge_request.update_attribute(:merge_error, 'Error')
+
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request.state).to eq('opened') }
+ it { expect(@merge_request.merge_error).not_to be_nil }
+ end
+
+ context 'MR can not be merged when note sha != MR sha' do
+ let(:opts) do
+ {
+ merge: 'other_commit'
+ }
+ end
+
+ before do
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request = MergeRequest.find(merge_request.id)
+ end
+ end
+
+ it { expect(@merge_request.state).to eq('opened') }
+ end
+ end
+
context 'todos' do
let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 25804696d2e..b0cc3ce5f5a 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -63,6 +63,17 @@ describe Notes::CreateService, services: true do
expect(note.note).to eq "HELLO\nWORLD"
end
end
+
+ describe '/merge with sha option' do
+ let(:note_text) { %(HELLO\n/merge\nWORLD) }
+ let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') }
+
+ it 'saves the note and exectues merge command' do
+ note = described_class.new(project, user, params).execute
+
+ expect(note.note).to eq "HELLO\nWORLD"
+ end
+ 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/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index e139be19140..caa23962519 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,145 +1,101 @@
require 'spec_helper'
describe Projects::UpdateService, services: true do
- describe :update_by_user do
- before do
- @user = create :user
- @admin = create :user, admin: true
- @project = create :project, creator_id: @user.id, namespace: @user.namespace
- @opts = {}
- end
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- context 'is private when updated to private' do
- before do
- @created_private = @project.private?
+ describe 'update_by_user' do
+ context 'when visibility_level is INTERNAL' do
+ it 'updates the project to internal' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- update_project(@project, @user, @opts)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_internal
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.private?).to be_truthy }
end
- context 'is internal when updated to internal' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- update_project(@project, @user, @opts)
+ context 'when visibility_level is PUBLIC' do
+ it 'updates the project to public' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_public
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.internal?).to be_truthy }
end
- context 'is public when updated to public' do
+ context 'when visibility levels are restricted to PUBLIC only' do
before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(@project, @user, @opts)
- end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.public?).to be_truthy }
- end
-
- context 'respect configured visibility restrictions setting' do
- before(:each) do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- context 'is private when updated to private' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- update_project(@project, @user, @opts)
+ context 'when visibility_level is INTERNAL' do
+ it 'updates the project to internal' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_internal
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.private?).to be_truthy }
end
- context 'is internal when updated to internal' do
- before do
- @created_private = @project.private?
+ context 'when visibility_level is PUBLIC' do
+ it 'does not update the project to public' do
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- update_project(@project, @user, @opts)
+ expect(result).to eq({ status: :error, message: 'Visibility level unallowed' })
+ expect(project).to be_private
end
- it { expect(@created_private).to be_truthy }
- it { expect(@project.internal?).to be_truthy }
- end
-
- context 'is private when updated to public' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(@project, @user, @opts)
+ context 'when updated by an admin' do
+ it 'updates the project to public' do
+ result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ expect(result).to eq({ status: :success })
+ expect(project).to be_public
+ end
end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.private?).to be_truthy }
- end
-
- context 'is public when updated to public by admin' do
- before do
- @created_private = @project.private?
-
- @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(@project, @admin, @opts)
- end
-
- it { expect(@created_private).to be_truthy }
- it { expect(@project.public?).to be_truthy }
end
end
end
- describe :visibility_level do
- let(:user) { create :user, admin: true }
+ describe 'visibility_level' do
let(:project) { create(:project, :internal) }
let(:forked_project) { create(:forked_project_with_submodules, :internal) }
- let(:opts) { {} }
before do
forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
forked_project.save
-
- @created_internal = project.internal?
- @fork_created_internal = forked_project.internal?
end
- context 'updates forks visibility level when parent set to more restrictive' do
- before do
- opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- update_project(project, user, opts).inspect
- end
+ it 'updates forks visibility level when parent set to more restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
+
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- it { expect(@created_internal).to be_truthy }
- it { expect(@fork_created_internal).to be_truthy }
- it { expect(project.private?).to be_truthy }
- it { expect(project.forks.first.private?).to be_truthy }
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
+
+ expect(project).to be_private
+ expect(forked_project.reload).to be_private
end
- context 'does not update forks visibility level when parent set to less restrictive' do
- before do
- opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- update_project(project, user, opts).inspect
- end
+ it 'does not update forks visibility level when parent set to less restrictive' do
+ opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
+
+ expect(project).to be_internal
+ expect(forked_project).to be_internal
- it { expect(@created_internal).to be_truthy }
- it { expect(@fork_created_internal).to be_truthy }
- it { expect(project.public?).to be_truthy }
- it { expect(project.forks.first.internal?).to be_truthy }
+ expect(update_project(project, admin, opts)).to eq({ status: :success })
+
+ expect(project).to be_public
+ expect(forked_project.reload).to be_internal
end
end
+ it 'returns an error result when record cannot be updated' do
+ result = update_project(project, admin, { name: 'foo&bar' })
+
+ expect(result).to eq({ status: :error, message: 'Project could not be updated' })
+ end
+
def update_project(project, user, opts)
- Projects::UpdateService.new(project, user, opts).execute
+ described_class.new(project, user, opts).execute
end
end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index becf627a4f5..66fc8fc360b 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -1,12 +1,13 @@
require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:project, :public) }
let(:developer) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
let(:bug) { create(:label, project: project, title: 'Bug') }
+ let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
before do
project.team << [developer, :developer]
@@ -210,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)
@@ -218,6 +259,14 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'merge command' do
+ it 'runs merge command if content contains /merge' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(merge: merge_request.diff_head_sha)
+ end
+ end
+
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -238,6 +287,64 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { merge_request }
end
+ context 'merge command' do
+ let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) }
+
+ it_behaves_like 'merge command' do
+ let(:content) { '/merge' }
+ let(:issuable) { merge_request }
+ end
+
+ context 'can not be merged when logged user does not have permissions' do
+ let(:service) { described_class.new(project, create(:user)) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { merge_request }
+ end
+ end
+
+ context 'can not be merged when sha does not match' do
+ let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { merge_request }
+ end
+ end
+
+ context 'when sha is missing' do
+ let(:service) { described_class.new(project, developer, {}) }
+
+ it 'precheck passes and returns merge command' do
+ _, updates = service.execute('/merge', merge_request)
+
+ expect(updates).to eq(merge: nil)
+ end
+ end
+
+ context 'issue can not be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { issue }
+ end
+ end
+
+ context 'non persisted merge request cant be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { build(:merge_request) }
+ end
+ end
+
+ context 'not persisted merge request can not be merged' do
+ it_behaves_like 'empty command' do
+ let(:content) { "/merge" }
+ let(:issuable) { build(:merge_request, source_project: project) }
+ end
+ end
+ end
+
it_behaves_like 'title command' do
let(:content) { '/title A brand new title' }
let(:issuable) { issue }
@@ -451,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/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 9fbb61565e3..690fe979492 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -10,7 +10,21 @@ describe Users::RefreshAuthorizedProjectsService do
create!(project: project, user: user, access_level: access_level)
end
- describe '#execute' do
+ describe '#execute', :redis do
+ it 'refreshes the authorizations using a lease' do
+ expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
+ and_return('foo')
+
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).
+ with(an_instance_of(String), 'foo')
+
+ expect(service).to receive(:execute_without_lease)
+
+ service.execute
+ end
+ end
+
+ describe '#execute_without_lease' do
before do
user.project_authorizations.delete_all
end
@@ -19,37 +33,23 @@ describe Users::RefreshAuthorizedProjectsService do
project2 = create(:empty_project)
to_remove = create_authorization(project2, user)
- expect(service).to receive(:update_with_lease).
+ expect(service).to receive(:update_authorizations).
with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
- service.execute
+ service.execute_without_lease
end
it 'sets the access level of a project to the highest available level' do
to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER)
- expect(service).to receive(:update_with_lease).
+ expect(service).to receive(:update_authorizations).
with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
- service.execute
+ service.execute_without_lease
end
it 'returns a User' do
- expect(service.execute).to be_an_instance_of(User)
- end
- end
-
- describe '#update_with_lease', :redis do
- it 'refreshes the authorizations using a lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
- and_return('foo')
-
- expect(Gitlab::ExclusiveLease).to receive(:cancel).
- with(an_instance_of(String), 'foo')
-
- expect(service).to receive(:update_authorizations).with([1], [])
-
- service.update_with_lease([1])
+ expect(service.execute_without_lease).to be_an_instance_of(User)
end
end
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/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb
index 279db3c5748..98eb57f8b54 100644
--- a/spec/support/reactive_caching_helpers.rb
+++ b/spec/support/reactive_caching_helpers.rb
@@ -3,31 +3,35 @@ module ReactiveCachingHelpers
([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':')
end
- def stub_reactive_cache(subject = nil, data = nil)
+ def alive_reactive_cache_key(subject, *qualifiers)
+ reactive_cache_key(subject, *(qualifiers + ['alive']))
+ end
+
+ def stub_reactive_cache(subject = nil, data = nil, *qualifiers)
allow(ReactiveCachingWorker).to receive(:perform_async)
allow(ReactiveCachingWorker).to receive(:perform_in)
- write_reactive_cache(subject, data) if data
+ write_reactive_cache(subject, data, *qualifiers) if data
end
- def read_reactive_cache(subject)
- Rails.cache.read(reactive_cache_key(subject))
+ def read_reactive_cache(subject, *qualifiers)
+ Rails.cache.read(reactive_cache_key(subject, *qualifiers))
end
- def write_reactive_cache(subject, data)
- start_reactive_cache_lifetime(subject)
- Rails.cache.write(reactive_cache_key(subject), data)
+ def write_reactive_cache(subject, data, *qualifiers)
+ start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(reactive_cache_key(subject, *qualifiers), data)
end
- def reactive_cache_alive?(subject)
- Rails.cache.read(reactive_cache_key(subject, 'alive'))
+ def reactive_cache_alive?(subject, *qualifiers)
+ Rails.cache.read(alive_reactive_cache_key(subject, *qualifiers))
end
- def invalidate_reactive_cache(subject)
- Rails.cache.delete(reactive_cache_key(subject, 'alive'))
+ def invalidate_reactive_cache(subject, *qualifiers)
+ Rails.cache.delete(alive_reactive_cache_key(subject, *qualifiers))
end
- def start_reactive_cache_lifetime(subject)
- Rails.cache.write(reactive_cache_key(subject, 'alive'), true)
+ def start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true)
end
def expect_reactive_cache_update_queued(subject)
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
index 3f8398a31e3..03fa0a66b9a 100644
--- a/spec/support/seed_helper.rb
+++ b/spec/support/seed_helper.rb
@@ -25,32 +25,32 @@ module SeedHelper
end
def create_bare_seeds
- system(git_env, *%W(git clone --bare #{GITLAB_URL}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}),
chdir: SEED_REPOSITORY_PATH,
out: '/dev/null',
err: '/dev/null')
end
def create_normal_seeds
- system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
out: '/dev/null',
err: '/dev/null')
end
def create_mutable_seeds
- system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
out: '/dev/null',
err: '/dev/null')
system(git_env, *%w(git branch -t feature origin/feature),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
- system(git_env, *%W(git remote add expendable #{GITLAB_URL}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
end
def create_broken_seeds
- system(git_env, *%W(git clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
out: '/dev/null',
err: '/dev/null')
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/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index 935ceef0680..d028d1251ad 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -35,6 +35,8 @@ captures/
# Intellij
*.iml
.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
.idea/libraries
# Keystore files
diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore
index 1e9158e2a85..e3923f96fce 100644
--- a/vendor/gitignore/Autotools.gitignore
+++ b/vendor/gitignore/Autotools.gitignore
@@ -1,6 +1,11 @@
# http://www.gnu.org/software/automake
Makefile.in
+/ar-lib
+/mdate-sh
+/py-compile
+/test-driver
+/ylwrap
# http://www.gnu.org/software/autoconf
@@ -9,10 +14,20 @@ Makefile.in
/autoscan-*.log
/aclocal.m4
/compile
+/config.guess
/config.h.in
+/config.sub
/configure
/configure.scan
/depcomp
/install-sh
/missing
/stamp-h1
+
+# https://www.gnu.org/software/libtool/
+
+/ltmain.sh
+
+# http://www.gnu.org/software/texinfo
+
+/texinfo.tex
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
index 0cc7e4b5275..27ada0591ec 100644
--- a/vendor/gitignore/CMake.gitignore
+++ b/vendor/gitignore/CMake.gitignore
@@ -4,4 +4,5 @@ CMakeScripts
Makefile
cmake_install.cmake
install_manifest.txt
+compile_commands.json
CTestTestfile.cmake
diff --git a/vendor/gitignore/Clojure.gitignore b/vendor/gitignore/Clojure.gitignore
index a9fe6fba80d..7657a270c45 100644..120000
--- a/vendor/gitignore/Clojure.gitignore
+++ b/vendor/gitignore/Clojure.gitignore
@@ -1,13 +1 @@
-pom.xml
-pom.xml.asc
-*.jar
-*.class
-/lib/
-/classes/
-/target/
-/checkouts/
-.lein-deps-sum
-.lein-repl-history
-.lein-plugins/
-.lein-failures
-.nrepl-port
+Leiningen.gitignore \ No newline at end of file
diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore
index 0f77d9e1d17..60571a0c383 100644
--- a/vendor/gitignore/CodeIgniter.gitignore
+++ b/vendor/gitignore/CodeIgniter.gitignore
@@ -4,3 +4,8 @@
*/cache/*
!*/cache/index.html
!*/cache/.htaccess
+
+user_guide_src/build/*
+user_guide_src/cilexer/build/*
+user_guide_src/cilexer/dist/*
+user_guide_src/cilexer/pycilexer.egg-info/*
diff --git a/vendor/gitignore/CommonLisp.gitignore b/vendor/gitignore/CommonLisp.gitignore
index 4806e580b60..e7de127b014 100644
--- a/vendor/gitignore/CommonLisp.gitignore
+++ b/vendor/gitignore/CommonLisp.gitignore
@@ -1,3 +1,17 @@
*.FASL
*.fasl
*.lisp-temp
+*.dfsl
+*.pfsl
+*.d64fsl
+*.p64fsl
+*.lx64fsl
+*.lx32fsl
+*.dx64fsl
+*.dx32fsl
+*.fx64fsl
+*.fx32fsl
+*.sx64fsl
+*.sx32fsl
+*.wx64fsl
+*.wx32fsl
diff --git a/vendor/gitignore/Coq.gitignore b/vendor/gitignore/Coq.gitignore
index d3083b3a605..f25a61d9964 100644
--- a/vendor/gitignore/Coq.gitignore
+++ b/vendor/gitignore/Coq.gitignore
@@ -1,3 +1,30 @@
-*.vo
+.*.aux
+*.a
+*.cma
+*.cmi
+*.cmo
+*.cmx
+*.cmxa
+*.cmxs
*.glob
+*.ml.d
+*.ml4.d
+*.mli.d
+*.mllib.d
+*.mlpack.d
+*.native
+*.o
*.v.d
+*.vio
+*.vo
+.coq-native/
+.csdp.cache
+.lia.cache
+.nia.cache
+.nlia.cache
+.nra.cache
+csdp.cache
+lia.cache
+nia.cache
+nlia.cache
+nra.cache
diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore
index 7c280441649..4b366585ddc 100644
--- a/vendor/gitignore/Dart.gitignore
+++ b/vendor/gitignore/Dart.gitignore
@@ -1,13 +1,19 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
-.buildlog
+
+# SDK 1.20 and later (no longer creates packages directories)
.packages
-.project
.pub/
build/
+
+# Older SDK versions
+# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
+.project
+.buildlog
**/packages/
+
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
diff --git a/vendor/gitignore/Elisp.gitignore b/vendor/gitignore/Elisp.gitignore
index 9b4291b7fe8..206569dc661 100644
--- a/vendor/gitignore/Elisp.gitignore
+++ b/vendor/gitignore/Elisp.gitignore
@@ -3,3 +3,9 @@
# Packaging
.cask
+
+# Backup files
+*~
+
+# Undo-tree save-files
+*.~undo-tree
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index 755b605549d..ac67aaf3243 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -3,3 +3,4 @@
/deps
erl_crash.dump
*.ez
+*.beam
diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore
index 0c96c9ad060..3ac7904dcd2 100644
--- a/vendor/gitignore/Global/Emacs.gitignore
+++ b/vendor/gitignore/Global/Emacs.gitignore
@@ -39,4 +39,7 @@ flycheck_*.el
/server/
# projectiles files
-.projectile \ No newline at end of file
+.projectile
+
+# directory configuration
+.dir-locals.el
diff --git a/vendor/gitignore/Global/IPythonNotebook.gitignore b/vendor/gitignore/Global/IPythonNotebook.gitignore
deleted file mode 100644
index 27c13510bf5..00000000000
--- a/vendor/gitignore/Global/IPythonNotebook.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-# Temporary data
-.ipynb_checkpoints/
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 0a254147875..e375c744b6d 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -6,6 +6,7 @@
.idea/tasks.xml
# Sensitive or high-churn files:
+.idea/dataSources/
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore
index 69c8c2b29ce..95ff2244c99 100644
--- a/vendor/gitignore/Global/SublimeText.gitignore
+++ b/vendor/gitignore/Global/SublimeText.gitignore
@@ -20,6 +20,9 @@ Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
+Package Control.merged-ca-bundle
+Package Control.user-ca-bundle
+oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore
index bdc04a0b529..42e7afc1005 100644
--- a/vendor/gitignore/Global/Vim.gitignore
+++ b/vendor/gitignore/Global/Vim.gitignore
@@ -1,6 +1,8 @@
# swap
-[._]*.s[a-w][a-z]
-[._]s[a-w][a-z]
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-v][a-z]
+[._]sw[a-p]
# session
Session.vim
# temporary
diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore
index d9960081c98..0511e2b51f0 100644
--- a/vendor/gitignore/Global/VisualStudioCode.gitignore
+++ b/vendor/gitignore/Global/VisualStudioCode.gitignore
@@ -2,3 +2,4 @@
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
+!.vscode/extensions.json
diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore
index a0d31452b0e..ba26afd9653 100644
--- a/vendor/gitignore/Global/Windows.gitignore
+++ b/vendor/gitignore/Global/Windows.gitignore
@@ -1,6 +1,7 @@
-# Windows image file caches
+# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
+ehthumbs_vista.db
# Folder config file
Desktop.ini
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index 397a0ed4acb..5e1047c9d78 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -26,5 +26,5 @@ _testmain.go
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
-# external packages folder
+# External packages folder
vendor/
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index 32858aad3c3..e44e0860405 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -1,5 +1,8 @@
*.class
+# BlueJ files
+*.ctxt
+
# Mobile Tools for Java (J2ME)
.mtj.tmp/
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index e7c594fa3e2..a2d1564060b 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -6,8 +6,8 @@ bootstrap/compiled.php
app/storage/
# Laravel 5 & Lumen specific
-bootstrap/cache/
public/storage
+storage/*.key
.env.*.php
.env.php
.env
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
index 1cdc9f7fd45..9af45b175ae 100644
--- a/vendor/gitignore/Maven.gitignore
+++ b/vendor/gitignore/Maven.gitignore
@@ -7,3 +7,6 @@ release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
+
+# Exclude maven wrapper
+!/.mvn/wrapper/maven-wrapper.jar
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index bc7fc55724c..9a439fcd988 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -42,3 +42,7 @@ jspm_packages
# Output of 'npm pack'
*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore
index ae2ad536abb..d41364ab18e 100644
--- a/vendor/gitignore/Perl.gitignore
+++ b/vendor/gitignore/Perl.gitignore
@@ -1,20 +1,34 @@
-/blib/
-/.build/
-_build/
-cover_db/
-inc/
-Build
!Build/
-Build.bat
.last_cover_stats
-/Makefile
-/Makefile.old
-/MANIFEST.bak
/META.yml
/META.json
/MYMETA.*
-nytprof.out
-/pm_to_blib
*.o
*.bs
+
+# Devel::Cover
+cover_db/
+
+# Devel::NYTProf
+nytprof.out
+
+# Dizt::Zilla
+/.build/
+
+# Module::Build
+_build/
+Build
+Build.bat
+
+# Module::Install
+inc/
+
+# ExtUitls::MakeMaker
+/blib/
/_eumm/
+/*.gz
+/Makefile
+/Makefile.old
+/MANIFEST.bak
+/pm_to_blib
+/*.zip
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 6a2bf47ade9..9a05e2debe5 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -20,6 +20,7 @@ lib64/
parts/
sdist/
var/
+wheels/
*.egg-info/
.installed.cfg
*.egg
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
index 7d56f982f81..ed4d3c6c28d 100644
--- a/vendor/gitignore/Symfony.gitignore
+++ b/vendor/gitignore/Symfony.gitignore
@@ -31,9 +31,6 @@
/web/bundles/
/web/uploads/
-# Assets managed by Bower
-/web/assets/vendor/
-
# PHPUnit
/app/phpunit.xml
/phpunit.xml
@@ -45,4 +42,4 @@
/composer.phar
# Backup entities generated with doctrine:generate:entities command
-*/Entity/*~
+**/Entity/*~
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 1afbaf197f4..69bfb1eec3e 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -52,12 +52,22 @@ acs-*.bib
# beamer
*.nav
+*.pre
*.snm
*.vrb
+# changes
+*.soc
+
# cprotect
*.cpt
+# elsarticle (documentclass of Elsevier journals)
+*.spl
+
+# endnotes
+*.ent
+
# fixme
*.lox
@@ -123,9 +133,7 @@ acs-*.bib
*.maf
*.mlf
*.mlt
-*.mtc
-*.mtc[0-9]
-*.mtc[1-9][0-9]
+*.mtc[0-9]*
# minted
_minted*
@@ -140,6 +148,9 @@ _minted*
# nomencl
*.nlo
+# pax
+*.pax
+
# sagetex
*.sagetex.sage
*.sagetex.py
@@ -202,5 +213,8 @@ TSWLatexianTemp*
# KBibTeX
*~[0-9]*
-# auto folder when using emacs and auctex
+# auto folder when using emacs and auctex
/auto/*
+
+# expex forward references with \gathertags
+*-tags.tex
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 09e407344ca..d9e876cfcdd 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -8,7 +8,6 @@
*.user
*.userosscache
*.sln.docstates
-*.vcxproj.filters
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -43,11 +42,11 @@ TestResult.xml
[Rr]eleasePS/
dlldata.c
-# DNX
+# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
-Properties/launchSettings.json
+**/Properties/launchSettings.json
*_i.c
*_p.c
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
index f3fa3949656..8c590579934 100644
--- a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -7,6 +7,7 @@ services:
build:
stage: build
script:
+ - export IMAGE_TAG=$(echo -en $CI_BUILD_REF_NAME | tr -c '[:alnum:]_.-' '-')
- docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY
- - docker build --pull -t "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME" .
- - docker push "$CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME"
+ - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" .
+ - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG"
diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
index 00f9541e89b..981a77497e2 100644
--- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
@@ -1,6 +1,4 @@
-# This template uses the non default language docker image
-# The image already has Hex installed. You might want to consider to use `elixir:latest`
-image: trenpixster/elixir:latest
+image: elixir:latest
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
@@ -11,6 +9,8 @@ services:
- postgres:latest
before_script:
+ - mix local.rebar --force
+ - mix local.hex --force
- mix deps.get
mix:
diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
new file mode 100644
index 00000000000..e23b6e212f0
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
@@ -0,0 +1,37 @@
+image: golang:latest
+
+# The problem is that to be able to use go get, one needs to put
+# the repository in the $GOPATH. So for example if your gitlab domain
+# is mydomainperso.com, and that your repository is repos/projectname, and
+# the default GOPATH being /go, then you'd need to have your
+# repository in /go/src/mydomainperso.com/repos/projectname
+# Thus, making a symbolic link corrects this.
+before_script:
+ - ln -s /builds /go/src/mydomainperso.com
+ - cd /go/src/mydomainperso.com/repos/projectname
+
+stages:
+ - test
+ - build
+
+format:
+ stage: test
+ script:
+ # Add here all the dependencies, or use glide/govendor to get
+ # them automatically.
+ # - curl https://glide.sh/get | sh
+ - go get github.com/alecthomas/kingpin
+ - go tool vet -composites=false -shadow=true *.go
+ - go test -race $(go list ./... | grep -v /vendor/)
+
+compile:
+ stage: build
+ script:
+ # Add here all the dependencies, or use glide/govendor/...
+ # to get them automatically.
+ - go get github.com/alecthomas/kingpin
+ # Better put this in a Makefile
+ - go build -race -ldflags "-extldflags '-static'" -o mybinary
+ artifacts:
+ paths:
+ - mybinary
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
index 263c4c19999..98d3039ad06 100644
--- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -31,4 +31,4 @@ build:
test:
stage: test
script:
- - ./gradlew -g /cache./gradle check
+ - ./gradlew -g /cache/.gradle check
diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
new file mode 100644
index 00000000000..2ba5cad9682
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
@@ -0,0 +1,92 @@
+# This file is a template, and might need editing before it works on your project.
+image: ayufan/openshift-cli
+
+stages:
+ - test
+ - review
+ - staging
+ - production
+
+variables:
+ OPENSHIFT_SERVER: openshift.default.svc.cluster.local
+ # OPENSHIFT_DOMAIN: apps.example.com
+ # Configure this variable in Secure Variables:
+ # OPENSHIFT_TOKEN: my.openshift.token
+
+test1:
+ stage: test
+ before_script: []
+ script:
+ - echo run tests
+
+test2:
+ stage: test
+ before_script: []
+ script:
+ - echo run tests
+
+.deploy: &deploy
+ before_script:
+ - oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify
+ - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME"
+ script:
+ - "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
+ - "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
+ - "oc get routes $APP 2> /dev/null || oc expose service $APP --hostname=$APP_HOST"
+
+review:
+ <<: *deploy
+ stage: review
+ variables:
+ APP: $CI_BUILD_REF_NAME
+ APP_HOST: $CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: http://$CI_PROJECT_NAME-$CI_BUILD_REF_NAME.$OPENSHIFT_DOMAIN
+ on_stop: stop-review
+ only:
+ - branches
+ except:
+ - master
+
+stop-review:
+ <<: *deploy
+ stage: review
+ script:
+ - oc delete all -l "app=$APP"
+ when: manual
+ variables:
+ APP: $CI_BUILD_REF_NAME
+ GIT_STRATEGY: none
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+ only:
+ - branches
+ except:
+ - master
+
+staging:
+ <<: *deploy
+ stage: staging
+ variables:
+ APP: staging
+ APP_HOST: $CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
+ environment:
+ name: staging
+ url: http://$CI_PROJECT_NAME-staging.$OPENSHIFT_DOMAIN
+ only:
+ - master
+
+production:
+ <<: *deploy
+ stage: production
+ variables:
+ APP: production
+ APP_HOST: $CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
+ when: manual
+ environment:
+ name: production
+ url: http://$CI_PROJECT_NAME.$OPENSHIFT_DOMAIN
+ only:
+ - master
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
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
index e384b585ae0..249adbc9f4a 100644
--- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
@@ -1,3 +1,5 @@
+# Explaination on the scripts:
+# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md
image: registry.gitlab.com/gitlab-examples/openshift-deploy
variables: